mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-05 07:56:17 +00:00
Compare commits
35 Commits
v2.0.0-alp
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
e983a7b8f3 | ||
|
c33eaba1c0 | ||
|
3774257abb | ||
|
588f09bdaa | ||
|
7557a6ef5a | ||
|
3478645317 | ||
|
a950dd76ba | ||
|
8c0ecec485 | ||
|
d01d865b4d | ||
|
1b8cdc3417 | ||
|
d35889de73 | ||
|
0b18b5efd6 | ||
|
2cf2341e4c | ||
|
043d25a08f | ||
|
f6c8cd5ea8 | ||
|
a04eaa4bfb | ||
|
7a0a2117f5 | ||
|
2cea2e477a | ||
|
c2658534b0 | ||
|
2030c59362 | ||
|
e31c170f48 | ||
|
49a987cbce | ||
|
3526240faf | ||
|
075fd0171e | ||
|
c73ce0288e | ||
|
31c0daeba8 | ||
|
662e9c0549 | ||
|
6523a87dfb | ||
|
7ccec5db8d | ||
|
c211c56f75 | ||
|
17844ed929 | ||
|
2d78fe33b8 | ||
|
63d85d8123 | ||
|
26d3257516 | ||
|
d596f578f6 |
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve WG-Portal
|
||||
labels: bug
|
||||
|
||||
---
|
||||
<!-- Tip: you can use code blocks
|
||||
for better better formatting of yaml config or logs
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
```
|
||||
|
||||
```console
|
||||
logs here
|
||||
``` -->
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Steps to reproduce**
|
||||
<!--Steps to reproduce the bug should be clear and easily reproducible to help people
|
||||
gain an understanding of the problem.-->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
- Application version: v
|
||||
- Install method: binary/docker/helm/sources
|
||||
<!-- - OS: -->
|
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
labels: 'enhancement'
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
18
.github/pull_request_template.md
vendored
Normal file
18
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
## Problem Statement
|
||||
|
||||
What is the problem you're trying to solve?
|
||||
|
||||
## Related Issue
|
||||
|
||||
Fixes #...
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
How do you like to solve the issue and why?
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Commits are signed with `git commit --signoff`
|
||||
- [ ] Changes have reasonable test coverage
|
||||
- [ ] Tests pass with `make test`
|
||||
- [ ] Helm docs are up-to-date with `make helm-docs`
|
9
.github/workflows/docker-publish.yml
vendored
9
.github/workflows/docker-publish.yml
vendored
@@ -59,10 +59,15 @@ jobs:
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
# semver tags, without v prefix
|
||||
type=semver,pattern={{version}}
|
||||
# major and major.minor tags are not available for alpha or beta releases
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
type=semver,pattern={{major}}
|
||||
# add v{{major}} tag, even for beta releases
|
||||
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') }}
|
||||
# add {{major}} tag, even for beta releases
|
||||
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') }}
|
||||
# set latest tag for default branch
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
|
2
.github/workflows/pages.yml
vendored
2
.github/workflows/pages.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
python-version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin
|
||||
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
||||
|
||||
- name: Publish documentation
|
||||
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -32,11 +32,11 @@ ssh.key
|
||||
.testCoverage.txt
|
||||
wg_portal.db
|
||||
sqlite.db
|
||||
swagger.json
|
||||
swagger.yaml
|
||||
/config.yml
|
||||
/config/
|
||||
venv/
|
||||
.cache/
|
||||
# ignore local frontend dist directory
|
||||
internal/app/api/core/frontend-dist
|
||||
# mkdocs output directory
|
||||
site/
|
||||
|
6
Makefile
6
Makefile
@@ -133,3 +133,9 @@ build-docker:
|
||||
.PHONY: helm-docs
|
||||
helm-docs:
|
||||
docker run --rm --volume "${PWD}/deploy:/helm-docs" -u "$$(id -u)" jnorwood/helm-docs -s file
|
||||
|
||||
#< run-mkdocs: Run a local instance of MkDocs
|
||||
.PHONY: run-mkdocs
|
||||
run-mkdocs:
|
||||
python -m venv venv; source venv/bin/activate; pip install mike cairosvg mkdocs-material mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
||||
venv/bin/mkdocs serve
|
||||
|
265
README.md
265
README.md
@@ -1,253 +1,74 @@
|
||||
# WireGuard Portal (v2 - testing)
|
||||
|
||||
[](https://github.com/h44z/wg-portal/actions)
|
||||
[](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||

|
||||

|
||||
[](https://goreportcard.com/report/github.com/h44z/wg-portal)
|
||||

|
||||

|
||||
[](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
|
||||
in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
|
||||
> [!CAUTION]
|
||||
> Version 2 is currently under development and may contain bugs and breaking changes.
|
||||
> It is not advised to use this version in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
|
||||
|
||||
Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to: https://hub.docker.com/r/wgportal/wg-portal.
|
||||
Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
|
||||
> [!IMPORTANT]
|
||||
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
|
||||
> Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
* 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~~ (coming soon)
|
||||
|
||||

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

|
||||
|
||||
## Configuration
|
||||
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. |
|
||||
| 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 and is_admin. |
|
||||
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
||||
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
|
||||
| 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. |
|
||||
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
|
||||
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
|
||||
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
|
||||
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
|
||||
| tls_key_path | auth/ldap | | A path to the TLS key. |
|
||||
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
|
||||
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
|
||||
| bind_pass | auth/ldap | | The bind password. |
|
||||
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
|
||||
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
|
||||
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
|
||||
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
|
||||
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
|
||||
| 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. |
|
||||
| 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 |
|
||||
|
||||
## 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!
|
||||
## Documentation
|
||||
|
||||
For the complete documentation visit [wgportal.org](https://wgportal.org).
|
||||
|
||||
## V2 TODOs
|
||||
* Public REST API
|
||||
* Translations
|
||||
* Documentation
|
||||
* Audit UI
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
To build a standalone application, use the Makefile provided in the repository.
|
||||
Go version 1.22 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
|
||||
```
|
||||
* Audit UI
|
||||
|
||||
## 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
|
||||
|
||||
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
|
||||
* [Gin](https://github.com/gin-gonic/gin), HTTP web framework written in Go
|
||||
* [Bootstrap](https://getbootstrap.com/), for the HTML templates
|
||||
* [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
|
||||
```
|
||||
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
|
||||
* [Gin](https://github.com/gin-gonic/gin), HTTP web framework written in Go
|
||||
* [Bootstrap](https://getbootstrap.com/), for the HTML templates
|
||||
* [Vue.JS](https://vuejs.org/), for the frontend
|
||||
|
||||
## License
|
||||
|
||||
* MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT
|
||||
* MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT>
|
||||
|
@@ -7,11 +7,15 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/swaggo/swag"
|
||||
"github.com/swaggo/swag/gen"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var apiRootPath = "/internal/app/api"
|
||||
var apiDocPath = "core/assets/doc"
|
||||
var apiMkDocPath = "/docs/documentation/rest-api"
|
||||
|
||||
// this replaces the call to: swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo base.go
|
||||
func main() {
|
||||
wd, err := os.Getwd() // should be the project root
|
||||
@@ -19,10 +23,9 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
apiBasePath := filepath.Join(wd, "/internal/app/api")
|
||||
apis := []string{"v0"}
|
||||
apiBasePath := filepath.Join(wd, apiRootPath)
|
||||
apis := []string{"v0", "v1"}
|
||||
|
||||
hasError := false
|
||||
for _, apiVersion := range apis {
|
||||
apiPath := filepath.Join(apiBasePath, apiVersion, "handlers")
|
||||
|
||||
@@ -33,16 +36,20 @@ func main() {
|
||||
|
||||
err := generateApi(apiBasePath, apiPath, apiVersion)
|
||||
if err != nil {
|
||||
hasError = true
|
||||
logrus.Errorf("failed to generate API docs for %s: %v", apiVersion, err)
|
||||
log.Fatalf("failed to generate API docs for %s: %v", apiVersion, err)
|
||||
}
|
||||
|
||||
// copy the latest version of the API docs for mkdocs
|
||||
if apiVersion == apis[len(apis)-1] {
|
||||
if err = copyDocForMkdocs(wd, apiBasePath, apiVersion); err != nil {
|
||||
log.Printf("failed to copy API docs for mkdocs: %v", err)
|
||||
} else {
|
||||
log.Println("Copied API docs " + apiVersion + " for mkdocs")
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Generated swagger docs for API", apiVersion)
|
||||
}
|
||||
|
||||
if hasError {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func generateApi(basePath, apiPath, version string) error {
|
||||
@@ -51,7 +58,7 @@ func generateApi(basePath, apiPath, version string) error {
|
||||
Excludes: "",
|
||||
MainAPIFile: "base.go",
|
||||
PropNamingStrategy: swag.PascalCase,
|
||||
OutputDir: filepath.Join(basePath, "core/assets/doc"),
|
||||
OutputDir: filepath.Join(basePath, apiDocPath),
|
||||
OutputTypes: []string{"json", "yaml"},
|
||||
ParseVendor: false,
|
||||
ParseDependency: 3,
|
||||
@@ -68,3 +75,43 @@ func generateApi(basePath, apiPath, version string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDocForMkdocs(workingDir, basePath, version string) error {
|
||||
srcPath := filepath.Join(basePath, apiDocPath, fmt.Sprintf("%s_swagger.yaml", version))
|
||||
dstPath := filepath.Join(workingDir, apiMkDocPath, "swagger.yaml")
|
||||
|
||||
// copy the file
|
||||
input, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading swagger doc: %w", err)
|
||||
}
|
||||
|
||||
output, err := removeAuthorizeButton(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while removing authorize button: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(dstPath, output, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing swagger doc: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeAuthorizeButton(input []byte) ([]byte, error) {
|
||||
var swagger map[string]interface{}
|
||||
err := yaml.Unmarshal(input, &swagger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while unmarshalling swagger file: %w", err)
|
||||
}
|
||||
|
||||
delete(swagger, "securityDefinitions")
|
||||
|
||||
output, err := yaml.Marshal(&swagger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while marshalling swagger file: %w", err)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
@@ -9,6 +9,8 @@ import (
|
||||
|
||||
"github.com/h44z/wg-portal/internal/app/api/core"
|
||||
handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers"
|
||||
backendV1 "github.com/h44z/wg-portal/internal/app/api/v1/backend"
|
||||
handlersV1 "github.com/h44z/wg-portal/internal/app/api/v1/handlers"
|
||||
"github.com/h44z/wg-portal/internal/app/audit"
|
||||
"github.com/h44z/wg-portal/internal/app/auth"
|
||||
"github.com/h44z/wg-portal/internal/app/configfile"
|
||||
@@ -103,7 +105,27 @@ func main() {
|
||||
|
||||
apiFrontend := handlersV0.NewRestApi(cfg, backend)
|
||||
|
||||
webSrv, err := core.NewServer(cfg, apiFrontend)
|
||||
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
|
||||
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
|
||||
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
|
||||
apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager)
|
||||
apiV1BackendMetrics := backendV1.NewMetricsService(cfg, database, userManager, wireGuardManager)
|
||||
apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1BackendUsers)
|
||||
apiV1EndpointPeers := handlersV1.NewPeerEndpoint(apiV1BackendPeers)
|
||||
apiV1EndpointInterfaces := handlersV1.NewInterfaceEndpoint(apiV1BackendInterfaces)
|
||||
apiV1EndpointProvisioning := handlersV1.NewProvisioningEndpoint(apiV1BackendProvisioning)
|
||||
apiV1EndpointMetrics := handlersV1.NewMetricsEndpoint(apiV1BackendMetrics)
|
||||
|
||||
apiV1 := handlersV1.NewRestApi(
|
||||
userManager,
|
||||
apiV1EndpointUsers,
|
||||
apiV1EndpointPeers,
|
||||
apiV1EndpointInterfaces,
|
||||
apiV1EndpointProvisioning,
|
||||
apiV1EndpointMetrics,
|
||||
)
|
||||
|
||||
webSrv, err := core.NewServer(cfg, apiFrontend, apiV1)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
go metricsServer.Run(ctx)
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# More information about the configuration can be found in the documentation: https://wgportal.org/master/documentation/overview/
|
||||
|
||||
advanced:
|
||||
log_level: trace
|
||||
|
||||
@@ -22,7 +24,7 @@ auth:
|
||||
base_dn: DC=YOURCOMPANY,DC=LOCAL
|
||||
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||
admin_group: CN=WireGuardAdmins,OU=it,DC=YOURCOMPANY,DC=LOCAL
|
||||
synchronize: false
|
||||
sync_interval: 0 # sync disabled
|
||||
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||
registration_enabled: true
|
||||
oidc:
|
||||
@@ -63,5 +65,28 @@ auth:
|
||||
email: email
|
||||
firstname: name
|
||||
user_identifier: sub
|
||||
is_admin: roles
|
||||
registration_enabled: true
|
||||
is_admin: this-attribute-must-be-true
|
||||
registration_enabled: true
|
||||
- id: google_plain_oauth_with_groups
|
||||
provider_name: google4
|
||||
display_name: Login with</br>Google4
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||
token_url: https://oauth2.googleapis.com/token
|
||||
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
- i-want-some-groups
|
||||
field_map:
|
||||
email: email
|
||||
firstname: name
|
||||
user_identifier: sub
|
||||
user_groups: groups
|
||||
admin_mapping:
|
||||
admin_value_regex: ^true$
|
||||
admin_group_regex: ^admin-group-name$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
@@ -2,10 +2,10 @@ apiVersion: v2
|
||||
name: wg-portal
|
||||
description: WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
||||
# Version is set to ensure compatibility with the chart's Ingress resource.
|
||||
kubeVersion: '>=1.19.0'
|
||||
kubeVersion: ">=1.19.0"
|
||||
type: application
|
||||
home: https://wgportal.org
|
||||
icon: https://wgportal.org/assets/images/logo.svg
|
||||
icon: https://wgportal.org/latest/assets/images/logo.svg
|
||||
sources:
|
||||
- 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
|
||||
# to the chart and its templates, including the app version.
|
||||
# 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
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: latest
|
||||
appVersion: "v2"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# wg-portal
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
||||
|
||||
@@ -27,96 +27,97 @@ The [Values](#values) section lists the parameters that can be configured during
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| nameOverride | string | `""` | Partially override resource names (adds suffix) |
|
||||
| fullnameOverride | string | `""` | Fully override resource names |
|
||||
| extraDeploy | list | `[]` | Array of extra objects to deploy with the release |
|
||||
| config.advanced | tpl/object | `{}` | Advanced configuration options. |
|
||||
| config.auth | tpl/object | `{}` | Auth configuration 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.database | tpl/object | `{}` | Database configuration options |
|
||||
| config.mail | tpl/object | `{}` | Mail configuration options |
|
||||
| config.statistics | tpl/object | `{}` | Statistics configuration 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. |
|
||||
| revisionHistoryLimit | string | `10` | The number of old ReplicaSets to retain to allow rollback. |
|
||||
| workloadType | string | `"Deployment"` | Workload type - `Deployment` or `StatefulSet` |
|
||||
| strategy | object | `{"type":"RollingUpdate"}` | Update strategy for the workload Valid values are: `RollingUpdate` or `Recreate` for Deployment, `RollingUpdate` or `OnDelete` for StatefulSet |
|
||||
| image.repository | string | `"ghcr.io/h44z/wg-portal"` | Image repository |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
|
||||
| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion |
|
||||
| imagePullSecrets | list | `[]` | Image pull secrets |
|
||||
| podAnnotations | tpl/object | `{}` | Extra annotations to add to the pod |
|
||||
| podLabels | object | `{}` | Extra labels to add to the pod |
|
||||
| podSecurityContext | object | `{}` | Pod Security Context |
|
||||
| securityContext.capabilities.add | list | `["NET_ADMIN"]` | Add capabilities to the container |
|
||||
| initContainers | tpl/list | `[]` | Pod init containers |
|
||||
| sidecarContainers | tpl/list | `[]` | Pod sidecar containers |
|
||||
| dnsPolicy | string | `"ClusterFirst"` | Set DNS policy for the pod. Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`. |
|
||||
| restartPolicy | string | `"Always"` | Restart policy for all containers within the pod. Valid values are `Always`, `OnFailure` or `Never`. |
|
||||
| hostNetwork | string | `false`. | Use the host's network namespace. |
|
||||
| resources | object | `{}` | Resources requests and limits |
|
||||
| command | list | `[]` | Overwrite pod command |
|
||||
| args | list | `[]` | Additional pod arguments |
|
||||
| env | tpl/list | `[]` | Additional environment variables |
|
||||
| envFrom | tpl/list | `[]` | Additional environment variables from a secret or configMap |
|
||||
| livenessProbe | object | `{}` | Liveness probe configuration |
|
||||
| readinessProbe | object | `{}` | Readiness probe configuration |
|
||||
| startupProbe | object | `{}` | Startup probe configuration |
|
||||
| volumes | tpl/list | `[]` | Additional volumes |
|
||||
| volumeMounts | tpl/list | `[]` | Additional volumeMounts |
|
||||
| nodeSelector | object | `{"kubernetes.io/os":"linux"}` | Node Selector configuration |
|
||||
| tolerations | list | `[]` | Tolerations configuration |
|
||||
| affinity | object | `{}` | Affinity configuration |
|
||||
| service.mixed.enabled | bool | `false` | Whether to create a single service for the web and wireguard interfaces |
|
||||
| service.mixed.type | string | `"LoadBalancer"` | Service type |
|
||||
| service.web.annotations | object | `{}` | Annotations for the web service |
|
||||
| service.web.type | string | `"ClusterIP"` | Web service type |
|
||||
| service.web.port | int | `8888` | Web service port Used for the web interface listener |
|
||||
| service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service |
|
||||
| service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type |
|
||||
| service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. |
|
||||
| service.metrics.port | int | `8787` | |
|
||||
| ingress.enabled | bool | `false` | Specifies whether an ingress resource should be created |
|
||||
| ingress.className | string | `""` | Ingress class name |
|
||||
| ingress.annotations | object | `{}` | Ingress annotations |
|
||||
| ingress.tls | bool | `false` | Ingress TLS configuration. Enable certificate resource or add ingress annotation to create required secret |
|
||||
| certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created |
|
||||
| certificate.issuer.name | string | `""` | Certificate issuer name |
|
||||
| certificate.issuer.kind | string | `""` | Certificate issuer kind (ClusterIssuer or Issuer) |
|
||||
| certificate.issuer.group | string | `"cert-manager.io"` | Certificate issuer group |
|
||||
| certificate.duration | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.renewBefore | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.commonName | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.emailAddresses | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.ipAddresses | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.keystores | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.privateKey | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.secretTemplate | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.subject | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.uris | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.usages | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| persistence.enabled | bool | `false` | Specifies whether an persistent volume should be created |
|
||||
| persistence.annotations | object | `{}` | Persistent Volume Claim annotations |
|
||||
| persistence.storageClass | string | `""` | Persistent Volume storage class. If undefined (the default) cluster's default provisioner will be used. |
|
||||
| persistence.accessMode | string | `"ReadWriteOnce"` | Persistent Volume Access Mode |
|
||||
| persistence.size | string | `"1Gi"` | Persistent Volume size |
|
||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||
| serviceAccount.annotations | object | `{}` | Service account annotations |
|
||||
| serviceAccount.automount | bool | `false` | Automatically mount a ServiceAccount's API credentials |
|
||||
| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
|
||||
| monitoring.enabled | bool | `false` | Enable Prometheus monitoring. |
|
||||
| monitoring.apiVersion | string | `"monitoring.coreos.com/v1"` | API version of the Prometheus resource. Use `azmonitoring.coreos.com/v1` for Azure Managed Prometheus. |
|
||||
| monitoring.kind | string | `"PodMonitor"` | Kind of the Prometheus resource. Could be `PodMonitor` or `ServiceMonitor`. |
|
||||
| monitoring.labels | object | `{}` | Resource labels. |
|
||||
| monitoring.annotations | object | `{}` | Resource annotations. |
|
||||
| monitoring.interval | string | `1m` | Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used. |
|
||||
| monitoring.metricRelabelings | list | `[]` | Relabelings to samples before ingestion. |
|
||||
| monitoring.relabelings | list | `[]` | Relabelings to samples before scraping. |
|
||||
| monitoring.scrapeTimeout | string | `""` | Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used. |
|
||||
| monitoring.jobLabel | string | `""` | The label to use to retrieve the job name from. |
|
||||
| monitoring.podTargetLabels | object | `{}` | Transfers labels on the Kubernetes Pod onto the target. |
|
||||
| monitoring.dashboard.enabled | bool | `false` | Enable Grafana dashboard. |
|
||||
| monitoring.dashboard.annotations | object | `{}` | Annotations for the dashboard ConfigMap. |
|
||||
| monitoring.dashboard.labels | object | `{}` | Additional labels for the dashboard ConfigMap. |
|
||||
| monitoring.dashboard.namespace | string | `""` | Dashboard ConfigMap namespace Overrides the namespace for the dashboard ConfigMap. |
|
||||
| Key | Type | Default | Description |
|
||||
|----------------------------------|------------|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| nameOverride | string | `""` | Partially override resource names (adds suffix) |
|
||||
| fullnameOverride | string | `""` | Fully override resource names |
|
||||
| extraDeploy | list | `[]` | Array of extra objects to deploy with the release |
|
||||
| config.advanced | tpl/object | `{}` | [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options. |
|
||||
| config.auth | tpl/object | `{}` | [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options. |
|
||||
| config.core | tpl/object | `{}` | [Core configuration](https://wgportal.org/latest/documentation/configuration/overview/#core) options.<br> If external admins in `auth` are defined and there are no `admin_user` and `admin_password` defined here, the default admin account will be disabled. |
|
||||
| config.database | tpl/object | `{}` | [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options |
|
||||
| config.mail | tpl/object | `{}` | [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options |
|
||||
| config.statistics | tpl/object | `{}` | [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options |
|
||||
| config.web | tpl/object | `{}` | [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br> `listening_address` will be set automatically from `service.web.port`. `external_url` is required to enable ingress and certificate resources. |
|
||||
| revisionHistoryLimit | string | `10` | The number of old ReplicaSets to retain to allow rollback. |
|
||||
| workloadType | string | `"Deployment"` | Workload type - `Deployment` or `StatefulSet` |
|
||||
| strategy | object | `{"type":"RollingUpdate"}` | Update strategy for the workload Valid values are: `RollingUpdate` or `Recreate` for Deployment, `RollingUpdate` or `OnDelete` for StatefulSet |
|
||||
| image.repository | string | `"ghcr.io/h44z/wg-portal"` | Image repository |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
|
||||
| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion |
|
||||
| imagePullSecrets | list | `[]` | Image pull secrets |
|
||||
| podAnnotations | tpl/object | `{}` | Extra annotations to add to the pod |
|
||||
| podLabels | object | `{}` | Extra labels to add to the pod |
|
||||
| podSecurityContext | object | `{}` | Pod Security Context |
|
||||
| securityContext.capabilities.add | list | `["NET_ADMIN"]` | Add capabilities to the container |
|
||||
| initContainers | tpl/list | `[]` | Pod init containers |
|
||||
| sidecarContainers | tpl/list | `[]` | Pod sidecar containers |
|
||||
| dnsPolicy | string | `"ClusterFirst"` | Set DNS policy for the pod. Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`. |
|
||||
| restartPolicy | string | `"Always"` | Restart policy for all containers within the pod. Valid values are `Always`, `OnFailure` or `Never`. |
|
||||
| hostNetwork | string | `false`. | Use the host's network namespace. |
|
||||
| resources | object | `{}` | Resources requests and limits |
|
||||
| command | list | `[]` | Overwrite pod command |
|
||||
| args | list | `[]` | Additional pod arguments |
|
||||
| env | tpl/list | `[]` | Additional environment variables |
|
||||
| envFrom | tpl/list | `[]` | Additional environment variables from a secret or configMap |
|
||||
| livenessProbe | object | `{}` | Liveness probe configuration |
|
||||
| readinessProbe | object | `{}` | Readiness probe configuration |
|
||||
| startupProbe | object | `{}` | Startup probe configuration |
|
||||
| volumes | tpl/list | `[]` | Additional volumes |
|
||||
| volumeMounts | tpl/list | `[]` | Additional volumeMounts |
|
||||
| nodeSelector | object | `{"kubernetes.io/os":"linux"}` | Node Selector configuration |
|
||||
| tolerations | list | `[]` | Tolerations configuration |
|
||||
| affinity | object | `{}` | Affinity configuration |
|
||||
| service.mixed.enabled | bool | `false` | Whether to create a single service for the web and wireguard interfaces |
|
||||
| service.mixed.type | string | `"LoadBalancer"` | Service type |
|
||||
| service.web.annotations | object | `{}` | Annotations for the web service |
|
||||
| service.web.type | string | `"ClusterIP"` | Web service type |
|
||||
| service.web.port | int | `8888` | Web service port Used for the web interface listener |
|
||||
| service.web.appProtocol | string | `"http"` | Web service appProtocol. Will be auto set to `https` if certificate is enabled. |
|
||||
| service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service |
|
||||
| service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type |
|
||||
| service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. |
|
||||
| service.metrics.port | int | `8787` | |
|
||||
| ingress.enabled | bool | `false` | Specifies whether an ingress resource should be created |
|
||||
| ingress.className | string | `""` | Ingress class name |
|
||||
| ingress.annotations | object | `{}` | Ingress annotations |
|
||||
| ingress.tls | bool | `false` | Ingress TLS configuration. Enable certificate resource or add ingress annotation to create required secret |
|
||||
| certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created. If enabled, certificate will be used for the web. |
|
||||
| certificate.issuer.name | string | `""` | Certificate issuer name |
|
||||
| certificate.issuer.kind | string | `""` | Certificate issuer kind (ClusterIssuer or Issuer) |
|
||||
| certificate.issuer.group | string | `"cert-manager.io"` | Certificate issuer group |
|
||||
| certificate.duration | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.renewBefore | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.commonName | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.emailAddresses | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.ipAddresses | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.keystores | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.privateKey | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.secretTemplate | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.subject | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.uris | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.usages | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| persistence.enabled | bool | `false` | Specifies whether an persistent volume should be created |
|
||||
| persistence.annotations | object | `{}` | Persistent Volume Claim annotations |
|
||||
| persistence.storageClass | string | `""` | Persistent Volume storage class. If undefined (the default) cluster's default provisioner will be used. |
|
||||
| persistence.accessMode | string | `"ReadWriteOnce"` | Persistent Volume Access Mode |
|
||||
| persistence.size | string | `"1Gi"` | Persistent Volume size |
|
||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||
| serviceAccount.annotations | object | `{}` | Service account annotations |
|
||||
| serviceAccount.automount | bool | `false` | Automatically mount a ServiceAccount's API credentials |
|
||||
| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
|
||||
| monitoring.enabled | bool | `false` | Enable Prometheus monitoring. |
|
||||
| monitoring.apiVersion | string | `"monitoring.coreos.com/v1"` | API version of the Prometheus resource. Use `azmonitoring.coreos.com/v1` for Azure Managed Prometheus. |
|
||||
| monitoring.kind | string | `"PodMonitor"` | Kind of the Prometheus resource. Could be `PodMonitor` or `ServiceMonitor`. |
|
||||
| monitoring.labels | object | `{}` | Resource labels. |
|
||||
| monitoring.annotations | object | `{}` | Resource annotations. |
|
||||
| monitoring.interval | string | `1m` | Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used. |
|
||||
| monitoring.metricRelabelings | list | `[]` | Relabelings to samples before ingestion. |
|
||||
| monitoring.relabelings | list | `[]` | Relabelings to samples before scraping. |
|
||||
| monitoring.scrapeTimeout | string | `""` | Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used. |
|
||||
| monitoring.jobLabel | string | `""` | The label to use to retrieve the job name from. |
|
||||
| monitoring.podTargetLabels | object | `{}` | Transfers labels on the Kubernetes Pod onto the target. |
|
||||
| monitoring.dashboard.enabled | bool | `false` | Enable Grafana dashboard. |
|
||||
| monitoring.dashboard.annotations | object | `{}` | Annotations for the dashboard ConfigMap. |
|
||||
| monitoring.dashboard.labels | object | `{}` | Additional labels for the dashboard ConfigMap. |
|
||||
| monitoring.dashboard.namespace | string | `""` | Dashboard ConfigMap namespace Overrides the namespace for the dashboard ConfigMap. |
|
||||
|
@@ -62,9 +62,9 @@ Create the name of the service account to use
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Define default admin credentials
|
||||
Disables default admin credentials
|
||||
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" -}}
|
||||
{{- $externalAdmin := false -}}
|
||||
@@ -80,9 +80,8 @@ the admin_user and admin_password values are not used.
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not $externalAdmin -}}
|
||||
admin_user: admin@wgportal.local
|
||||
admin_password: {{ printf "%s/%s" .Release.Name .Release.Namespace | b64enc }}
|
||||
{{- if $externalAdmin -}}
|
||||
admin_user: ""
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
|
@@ -51,3 +51,16 @@ spec:
|
||||
{{- end }}
|
||||
selector: {{- include "wg-portal.selectorLabels" .context | nindent 4 }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Define the service port template for the web port
|
||||
*/}}
|
||||
{{- define "wg-portal.service.webPort" -}}
|
||||
name: web
|
||||
port: {{ .Values.service.web.port }}
|
||||
protocol: TCP
|
||||
targetPort: web
|
||||
{{- if semverCompare ">=1.20-0" .Capabilities.KubeVersion.Version }}
|
||||
appProtocol: {{ ternary "https" .Values.service.web.appProtocol .Values.certificate.enabled }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
@@ -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
|
||||
kind: Secret
|
||||
metadata:
|
||||
@@ -5,11 +13,9 @@ metadata:
|
||||
labels: {{- include "wg-portal.labels" . | nindent 4 }}
|
||||
stringData:
|
||||
config.yml: |
|
||||
advanced:
|
||||
start_listen_port: {{ .Values.service.wireguard.ports | sortAlpha | first }}
|
||||
{{- with .Values.config.advanced }}
|
||||
{{- tpl (toYaml (omit . "start_listen_port")) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with mustMerge $advanced .Values.config.advanced }}
|
||||
advanced: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
{{- with .Values.config.auth }}
|
||||
auth: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
@@ -27,14 +33,10 @@ stringData:
|
||||
mail: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
statistics:
|
||||
listening_address: :{{ .Values.service.metrics.port }}
|
||||
{{- with .Values.config.statistics }}
|
||||
{{- tpl (toYaml (omit . "listening_address")) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with mustMerge $statistics .Values.config.statistics }}
|
||||
statistics: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
web:
|
||||
listening_address: :{{ .Values.service.web.port }}
|
||||
{{- with .Values.config.web }}
|
||||
{{- tpl (toYaml (omit . "listening_address")) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- with mustMerge $web .Values.config.web }}
|
||||
web: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
|
@@ -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 -}}
|
||||
{{- range $idx, $port := .Values.service.wireguard.ports -}}
|
||||
{{- $name := printf "wg%d" $idx -}}
|
||||
|
@@ -3,37 +3,36 @@
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# -- Partially override resource names (adds suffix)
|
||||
nameOverride: ''
|
||||
nameOverride: ""
|
||||
# -- Fully override resource names
|
||||
fullnameOverride: ''
|
||||
fullnameOverride: ""
|
||||
# -- Array of extra objects to deploy with the release
|
||||
extraDeploy: []
|
||||
|
||||
# https://github.com/h44z/wg-portal/blob/master/README.md#configuration-options
|
||||
config:
|
||||
# -- (tpl/object) Advanced configuration options.
|
||||
# -- (tpl/object) [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options.
|
||||
advanced: {}
|
||||
# -- (tpl/object) Auth configuration options.
|
||||
# -- (tpl/object) [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options.
|
||||
auth: {}
|
||||
# -- (tpl/object) Core configuration options.<br>
|
||||
# If external admins in `auth` are not defined and
|
||||
# -- (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 credentials will be generated.
|
||||
# the default admin account will be disabled.
|
||||
core: {}
|
||||
# -- (tpl/object) Database configuration options
|
||||
# -- (tpl/object) [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options
|
||||
database: {}
|
||||
# -- (tpl/object) Mail configuration options
|
||||
# -- (tpl/object) [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options
|
||||
mail: {}
|
||||
# -- (tpl/object) Statistics configuration options
|
||||
# -- (tpl/object) [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options
|
||||
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`.
|
||||
# `external_url` is required to enable ingress and certificate resources.
|
||||
web: {}
|
||||
|
||||
# -- The number of old ReplicaSets to retain to allow rollback.
|
||||
# @default -- `10`
|
||||
revisionHistoryLimit: ''
|
||||
revisionHistoryLimit: ""
|
||||
# -- Workload type - `Deployment` or `StatefulSet`
|
||||
workloadType: Deployment
|
||||
# -- Update strategy for the workload
|
||||
@@ -49,7 +48,7 @@ image:
|
||||
# -- Image pull policy
|
||||
pullPolicy: IfNotPresent
|
||||
# -- Overrides the image tag whose default is the chart appVersion
|
||||
tag: ''
|
||||
tag: ""
|
||||
|
||||
# -- Image pull secrets
|
||||
imagePullSecrets: []
|
||||
@@ -73,14 +72,14 @@ sidecarContainers: []
|
||||
# -- Set DNS policy for the pod.
|
||||
# Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`.
|
||||
# @default -- `"ClusterFirst"`
|
||||
dnsPolicy: ''
|
||||
dnsPolicy: ""
|
||||
# -- Restart policy for all containers within the pod.
|
||||
# Valid values are `Always`, `OnFailure` or `Never`.
|
||||
# @default -- `"Always"`
|
||||
restartPolicy: ''
|
||||
restartPolicy: ""
|
||||
# -- Use the host's network namespace.
|
||||
# @default -- `false`.
|
||||
hostNetwork: ''
|
||||
hostNetwork: ""
|
||||
# -- Resources requests and limits
|
||||
resources: {}
|
||||
# -- Overwrite pod command
|
||||
@@ -123,6 +122,8 @@ service:
|
||||
# -- Web service port
|
||||
# Used for the web interface listener
|
||||
port: 8888
|
||||
# -- Web service appProtocol. Will be auto set to `https` if certificate is enabled.
|
||||
appProtocol: http
|
||||
wireguard:
|
||||
# -- Annotations for the WireGuard service
|
||||
annotations: {}
|
||||
@@ -141,7 +142,7 @@ ingress:
|
||||
# -- Specifies whether an ingress resource should be created
|
||||
enabled: false
|
||||
# -- Ingress class name
|
||||
className: ''
|
||||
className: ""
|
||||
# -- Ingress annotations
|
||||
annotations: {}
|
||||
# -- Ingress TLS configuration.
|
||||
@@ -149,21 +150,22 @@ ingress:
|
||||
tls: false
|
||||
|
||||
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
|
||||
issuer:
|
||||
# -- Certificate issuer name
|
||||
name: ''
|
||||
name: ""
|
||||
# -- Certificate issuer kind (ClusterIssuer or Issuer)
|
||||
kind: ''
|
||||
kind: ""
|
||||
# -- Certificate issuer group
|
||||
group: cert-manager.io
|
||||
# -- 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)
|
||||
renewBefore: ''
|
||||
renewBefore: ""
|
||||
# -- 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)
|
||||
emailAddresses: []
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
@@ -188,7 +190,7 @@ persistence:
|
||||
annotations: {}
|
||||
# -- Persistent Volume storage class.
|
||||
# If undefined (the default) cluster's default provisioner will be used.
|
||||
storageClass: ''
|
||||
storageClass: ""
|
||||
# -- Persistent Volume Access Mode
|
||||
accessMode: ReadWriteOnce
|
||||
# -- Persistent Volume size
|
||||
@@ -203,7 +205,7 @@ serviceAccount:
|
||||
automount: false
|
||||
# -- The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ''
|
||||
name: ""
|
||||
|
||||
monitoring:
|
||||
# -- Enable Prometheus monitoring.
|
||||
@@ -220,15 +222,15 @@ monitoring:
|
||||
annotations: {}
|
||||
# -- Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used.
|
||||
# @default -- `1m`
|
||||
interval: ''
|
||||
interval: ""
|
||||
# -- Relabelings to samples before ingestion.
|
||||
metricRelabelings: []
|
||||
# -- Relabelings to samples before scraping.
|
||||
relabelings: []
|
||||
# -- Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used.
|
||||
scrapeTimeout: ''
|
||||
scrapeTimeout: ""
|
||||
# -- The label to use to retrieve the job name from.
|
||||
jobLabel: ''
|
||||
jobLabel: ""
|
||||
# -- Transfers labels on the Kubernetes Pod onto the target.
|
||||
podTargetLabels: {}
|
||||
|
||||
@@ -241,4 +243,4 @@ monitoring:
|
||||
labels: {}
|
||||
# -- Dashboard ConfigMap namespace
|
||||
# Overrides the namespace for the dashboard ConfigMap.
|
||||
namespace: ''
|
||||
namespace: ""
|
||||
|
BIN
docs/assets/images/dashboard.png
Executable file
BIN
docs/assets/images/dashboard.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 269 KiB |
190
docs/documentation/configuration/examples.md
Normal file
190
docs/documentation/configuration/examples.md
Normal file
@@ -0,0 +1,190 @@
|
||||
Below are some sample YAML configurations demonstrating how to override some default values.
|
||||
|
||||
## Basic
|
||||
|
||||
```yaml
|
||||
core:
|
||||
admin_user: test@example.com
|
||||
admin_password: password
|
||||
admin_api_token: super-s3cr3t-api-token-or-a-UUID
|
||||
import_existing: false
|
||||
create_default_peer: true
|
||||
self_provisioning_allowed: true
|
||||
|
||||
web:
|
||||
site_title: My WireGuard Server
|
||||
site_company_name: My Company
|
||||
listening_address: :8080
|
||||
external_url: https://my.externa-domain.com
|
||||
csrf_secret: super-s3cr3t-csrf
|
||||
session_secret: super-s3cr3t-session
|
||||
request_logging: true
|
||||
|
||||
advanced:
|
||||
log_level: trace
|
||||
log_pretty: true
|
||||
log_json: false
|
||||
config_storage_path: /etc/wireguard
|
||||
expiry_check_interval: 5m
|
||||
|
||||
database:
|
||||
debug: true
|
||||
type: sqlite
|
||||
dsn: data/sqlite.db
|
||||
```
|
||||
|
||||
## LDAP Authentication and Synchronization
|
||||
|
||||
```yaml
|
||||
# ... (basic configuration)
|
||||
|
||||
auth:
|
||||
ldap:
|
||||
# a sample LDAP provider with user sync enabled
|
||||
- id: ldap
|
||||
provider_name: Active Directory
|
||||
display_name: Login with</br>AD
|
||||
url: ldap://srv-ad1.company.local:389
|
||||
bind_user: ldap_wireguard@company.local
|
||||
bind_pass: super-s3cr3t-ldap
|
||||
base_dn: DC=COMPANY,DC=LOCAL
|
||||
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||
sync_interval: 15m
|
||||
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||
disable_missing: true
|
||||
field_map:
|
||||
user_identifier: sAMAccountName
|
||||
email: mail
|
||||
firstname: givenName
|
||||
lastname: sn
|
||||
phone: telephoneNumber
|
||||
department: department
|
||||
memberof: memberOf
|
||||
admin_group: CN=WireGuardAdmins,OU=Some-OU,DC=COMPANY,DC=LOCAL
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
```
|
||||
|
||||
## OpenID Connect (OIDC) Authentication
|
||||
|
||||
```yaml
|
||||
# ... (basic configuration)
|
||||
|
||||
auth:
|
||||
oidc:
|
||||
# a sample Entra ID provider with environment variable substitution
|
||||
- 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
|
||||
provider_name: google
|
||||
display_name: Login with</br>Google
|
||||
base_url: https://accounts.google.com
|
||||
client_id: the-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
extra_scopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
- https://www.googleapis.com/auth/userinfo.profile
|
||||
field_map:
|
||||
user_identifier: sub
|
||||
email: email
|
||||
firstname: given_name
|
||||
lastname: family_name
|
||||
phone: phone_number
|
||||
department: department
|
||||
is_admin: wg_admin
|
||||
admin_mapping:
|
||||
admin_value_regex: ^true$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
|
||||
# a sample provider where users in the group `the-admin-group` are considered as admins
|
||||
- id: oidc-with-admin-group
|
||||
provider_name: google2
|
||||
display_name: Login with</br>Google2
|
||||
base_url: https://accounts.google.com
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
extra_scopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
- https://www.googleapis.com/auth/userinfo.profile
|
||||
field_map:
|
||||
user_identifier: sub
|
||||
email: email
|
||||
firstname: given_name
|
||||
lastname: family_name
|
||||
phone: phone_number
|
||||
department: department
|
||||
user_groups: groups
|
||||
admin_mapping:
|
||||
admin_group_regex: ^the-admin-group$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
```
|
||||
|
||||
## Plain OAuth2 Authentication
|
||||
|
||||
```yaml
|
||||
# ... (basic configuration)
|
||||
|
||||
auth:
|
||||
oauth:
|
||||
# a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True`
|
||||
# are considered as admins
|
||||
- id: google_plain_oauth-with-admin-attribute
|
||||
provider_name: google3
|
||||
display_name: Login with</br>Google3
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||
token_url: https://oauth2.googleapis.com/token
|
||||
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
field_map:
|
||||
user_identifier: sub
|
||||
email: email
|
||||
firstname: name
|
||||
is_admin: this-attribute-must-be-true
|
||||
admin_mapping:
|
||||
admin_value_regex: ^(True|true)$
|
||||
registration_enabled: true
|
||||
|
||||
# a sample provider where either users with the attribute `this-attribute-must-be-true` set to `true` or
|
||||
# users in the group `admin-group-name` are considered as admins
|
||||
- id: google_plain_oauth_with_groups
|
||||
provider_name: google4
|
||||
display_name: Login with</br>Google4
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||
token_url: https://oauth2.googleapis.com/token
|
||||
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
- i-want-some-groups
|
||||
field_map:
|
||||
email: email
|
||||
firstname: name
|
||||
user_identifier: sub
|
||||
is_admin: this-attribute-must-be-true
|
||||
user_groups: groups
|
||||
admin_mapping:
|
||||
admin_value_regex: ^true$
|
||||
admin_group_regex: ^admin-group-name$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
```
|
593
docs/documentation/configuration/overview.md
Normal file
593
docs/documentation/configuration/overview.md
Normal file
@@ -0,0 +1,593 @@
|
||||
This page provides an overview of **all available configuration options** for WireGuard Portal.
|
||||
|
||||
You can supply these configurations in a **YAML** file (e.g. `config.yaml`) when starting the Portal.
|
||||
The path of the configuration file defaults to **config/config.yml** in the working directory of the executable.
|
||||
It is possible to override configuration filepath using the environment variable `WG_PORTAL_CONFIG`.
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Core
|
||||
|
||||
These are the primary configuration options that control fundamental WireGuard Portal behavior.
|
||||
More advanced options are found in the subsequent `Advanced` section.
|
||||
|
||||
### `admin_user`
|
||||
- **Default:** `admin@wgportal.local`
|
||||
- **Description:** The administrator user. This user will be created as a default admin if it does not yet exist.
|
||||
|
||||
### `admin_password`
|
||||
- **Default:** `wgportal`
|
||||
- **Description:** The administrator password. The default password of `wgportal` should be changed immediately.
|
||||
|
||||
### `admin_api_token`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** An API token for the admin user. If a token is provided, the REST API can be accessed using this token. If empty, the API is initially disabled for the admin user.
|
||||
|
||||
### `editable_keys`
|
||||
- **Default:** `true`
|
||||
- **Description:** Allow editing of WireGuard key-pairs directly in the UI.
|
||||
|
||||
### `create_default_peer`
|
||||
- **Default:** `false`
|
||||
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||
|
||||
### `create_default_peer_on_creation`
|
||||
- **Default:** `false`
|
||||
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||
|
||||
### `re_enable_peer_after_user_enable`
|
||||
- **Default:** `true`
|
||||
- **Description:** Re-enable all peers that were previously disabled if the associated user is re-enabled.
|
||||
|
||||
### `delete_peer_after_user_deleted`
|
||||
- **Default:** `false`
|
||||
- **Description:** If a user is deleted, remove all linked peers. Otherwise, peers remain but are disabled.
|
||||
|
||||
### `self_provisioning_allowed`
|
||||
- **Default:** `false`
|
||||
- **Description:** Allow registered (non-admin) users to self-provision peers from their profile page.
|
||||
|
||||
### `import_existing`
|
||||
- **Default:** `true`
|
||||
- **Description:** On startup, import existing WireGuard interfaces and peers into WireGuard Portal.
|
||||
|
||||
### `restore_state`
|
||||
- **Default:** `true`
|
||||
- **Description:** Restore the WireGuard interface states (up/down) that existed before WireGuard Portal started.
|
||||
|
||||
---
|
||||
|
||||
## Advanced
|
||||
|
||||
Additional or more specialized configuration options for logging and interface creation details.
|
||||
|
||||
### `log_level`
|
||||
- **Default:** `info`
|
||||
- **Description:** The log level used by the application. Valid options are: `trace`, `debug`, `info`, `warn`, `error`.
|
||||
|
||||
### `log_pretty`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, log messages are colorized and formatted for readability (pretty-print).
|
||||
|
||||
### `log_json`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, log messages are structured in JSON format.
|
||||
|
||||
### `start_listen_port`
|
||||
- **Default:** `51820`
|
||||
- **Description:** The first port to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `start_cidr_v4`
|
||||
- **Default:** `10.11.12.0/24`
|
||||
- **Description:** The initial IPv4 subnet to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `start_cidr_v6`
|
||||
- **Default:** `fdfd:d3ad:c0de:1234::0/64`
|
||||
- **Description:** The initial IPv6 subnet to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `use_ip_v6`
|
||||
- **Default:** `true`
|
||||
- **Description:** Enable or disable IPv6 support.
|
||||
|
||||
### `config_storage_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Path to a directory where `wg-quick` style configuration files will be stored (if you need local filesystem configs).
|
||||
|
||||
### `expiry_check_interval`
|
||||
- **Default:** `15m`
|
||||
- **Description:** Interval after which existing peers are checked if they are expired. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `rule_prio_offset`
|
||||
- **Default:** `20000`
|
||||
- **Description:** Offset for IP route rule priorities when configuring routing.
|
||||
|
||||
### `route_table_offset`
|
||||
- **Default:** `20000`
|
||||
- **Description:** Offset for IP route table IDs when configuring routing.
|
||||
|
||||
### `api_admin_only`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
|
||||
|
||||
---
|
||||
|
||||
## Database
|
||||
|
||||
Configuration for the underlying database used by WireGuard Portal.
|
||||
Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
|
||||
|
||||
### `debug`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, logs all database statements (verbose).
|
||||
|
||||
### `slow_query_threshold`
|
||||
- **Default:** 0
|
||||
- **Description:** A time threshold (e.g., `100ms`) above which queries are considered slow and logged as warnings. If empty or zero, slow query logging is disabled. Format uses `s`, `ms` for seconds, milliseconds, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `type`
|
||||
- **Default:** `sqlite`
|
||||
- **Description:** The database type. Valid options: `sqlite`, `mssql`, `mysql`, `postgres`.
|
||||
|
||||
### `dsn`
|
||||
- **Default:** `data/sqlite.db`
|
||||
- **Description:** The Data Source Name (DSN) for connecting to the database.
|
||||
For example:
|
||||
```text
|
||||
user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Statistics
|
||||
|
||||
Controls how WireGuard Portal collects and reports usage statistics, including ping checks and Prometheus metrics.
|
||||
|
||||
### `use_ping_checks`
|
||||
- **Default:** `true`
|
||||
- **Description:** Enable periodic ping checks to verify that peers remain responsive.
|
||||
|
||||
### `ping_check_workers`
|
||||
- **Default:** `10`
|
||||
- **Description:** Number of parallel worker processes for ping checks.
|
||||
|
||||
### `ping_unprivileged`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `false`, ping checks run without root privileges. This is currently considered BETA.
|
||||
|
||||
### `ping_check_interval`
|
||||
- **Default:** `1m`
|
||||
- **Description:** Interval between consecutive ping checks for all peers. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `data_collection_interval`
|
||||
- **Default:** `1m`
|
||||
- **Description:** Interval between data collection cycles (bytes sent/received, handshake times, etc.). Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `collect_interface_data`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, collects interface-level data (bytes in/out) for monitoring and statistics.
|
||||
|
||||
### `collect_peer_data`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, collects peer-level data (bytes, last handshake, endpoint, etc.).
|
||||
|
||||
### `collect_audit_data`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, logs certain portal events (such as user logins) to the database.
|
||||
|
||||
### `listening_address`
|
||||
- **Default:** `:8787`
|
||||
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787`).
|
||||
|
||||
---
|
||||
|
||||
## Mail
|
||||
|
||||
Options for configuring email notifications or sending peer configurations via email.
|
||||
|
||||
### `host`
|
||||
- **Default:** `127.0.0.1`
|
||||
- **Description:** Hostname or IP of the SMTP server.
|
||||
|
||||
### `port`
|
||||
- **Default:** `25`
|
||||
- **Description:** Port number for the SMTP server.
|
||||
|
||||
### `encryption`
|
||||
- **Default:** `none`
|
||||
- **Description:** SMTP encryption type. Valid values: `none`, `tls`, `starttls`.
|
||||
|
||||
### `cert_validation`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, validate the SMTP server certificate (relevant if `encryption` = `tls`).
|
||||
|
||||
### `username`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Optional SMTP username for authentication.
|
||||
|
||||
### `password`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Optional SMTP password for authentication.
|
||||
|
||||
### `auth_type`
|
||||
- **Default:** `plain`
|
||||
- **Description:** SMTP authentication type. Valid values: `plain`, `login`, `crammd5`.
|
||||
|
||||
### `from`
|
||||
- **Default:** `Wireguard Portal <noreply@wireguard.local>`
|
||||
- **Description:** The default "From" address when sending emails.
|
||||
|
||||
### `link_only`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, emails only contain a link to WireGuard Portal, rather than attaching the full configuration.
|
||||
|
||||
---
|
||||
|
||||
## Auth
|
||||
|
||||
WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), and **LDAP** (`ldap`).
|
||||
Each can have multiple providers configured. Below are the relevant keys.
|
||||
|
||||
---
|
||||
|
||||
### OIDC
|
||||
|
||||
The `oidc` array contains a list of OpenID Connect providers.
|
||||
Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
||||
|
||||
#### `provider_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||
|
||||
#### `display_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A user-friendly name shown on the login page (e.g., "Login with Google").
|
||||
|
||||
#### `base_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OIDC provider’s base URL (e.g., `https://accounts.google.com`).
|
||||
|
||||
#### `client_id`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client ID from the OIDC provider.
|
||||
|
||||
#### `client_secret`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client secret from the OIDC provider.
|
||||
|
||||
#### `extra_scopes`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of additional OIDC scopes (e.g., `profile`, `email`).
|
||||
|
||||
#### `field_map`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
|
||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
||||
|
||||
| **Field** | **Typical OIDC Claim** | **Explanation** |
|
||||
| ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
||||
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
||||
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
||||
| `lastname` | `family_name` | The user’s last (family) name, typically provided by the IdP in the `family_name` claim. |
|
||||
| `phone` | `phone_number` | The user’s phone number. This may require additional scopes/permissions from the IdP to access. |
|
||||
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
|
||||
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
|
||||
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
|
||||
|
||||
#### `admin_mapping`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
|
||||
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
|
||||
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, a new user will be created in WireGuard Portal if not already present.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, OIDC user data is logged at the trace level upon login (for debugging).
|
||||
|
||||
---
|
||||
|
||||
### OAuth
|
||||
|
||||
The `oauth` array contains a list of plain OAuth2 providers.
|
||||
Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
||||
|
||||
#### `provider_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||
|
||||
#### `display_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A user-friendly name shown on the login page.
|
||||
|
||||
#### `client_id`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client ID for the provider.
|
||||
|
||||
#### `client_secret`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client secret for the provider.
|
||||
|
||||
#### `auth_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** URL of the authentication endpoint.
|
||||
|
||||
#### `token_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** URL of the token endpoint.
|
||||
|
||||
#### `user_info_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** URL of the user information endpoint.
|
||||
|
||||
#### `scopes`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of OAuth scopes.
|
||||
|
||||
#### `field_map`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
|
||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
||||
|
||||
| **Field** | **Typical Claim** | **Explanation** |
|
||||
| ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
||||
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
||||
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
||||
| `lastname` | `family_name` | The user’s last (family) name, typically provided by the IdP in the `family_name` claim. |
|
||||
| `phone` | `phone_number` | The user’s phone number. This may require additional scopes/permissions from the IdP to access. |
|
||||
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
|
||||
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
|
||||
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
|
||||
|
||||
#### `admin_mapping`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
|
||||
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
|
||||
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, new users are created automatically on successful login.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, logs user info at the trace level upon login.
|
||||
|
||||
---
|
||||
|
||||
### LDAP
|
||||
|
||||
The `ldap` array contains a list of LDAP authentication providers.
|
||||
Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
||||
|
||||
#### `url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The LDAP server URL (e.g., `ldap://srv-ad01.company.local:389`).
|
||||
|
||||
#### `start_tls`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, use STARTTLS to secure the LDAP connection.
|
||||
|
||||
#### `cert_validation`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, validate the LDAP server’s TLS certificate.
|
||||
|
||||
#### `tls_certificate_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Path to a TLS certificate if needed for LDAP connections.
|
||||
|
||||
#### `tls_key_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Path to the corresponding TLS certificate key.
|
||||
|
||||
#### `base_dn`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The base DN for user searches (e.g., `DC=COMPANY,DC=LOCAL`).
|
||||
|
||||
#### `bind_user`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The bind user for LDAP (e.g., `company\\ldap_wireguard` or `ldap_wireguard@company.local`).
|
||||
|
||||
#### `bind_pass`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The bind password for LDAP authentication.
|
||||
|
||||
#### `field_map`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Maps LDAP attributes to WireGuard Portal fields.
|
||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`.
|
||||
|
||||
| **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** |
|
||||
| -------------------------- | -------------------------- | ------------------------------------------------------------ |
|
||||
| user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. |
|
||||
| email | mail / userPrincipalName | Stores the user's primary email address. |
|
||||
| firstname | givenName | Contains the user's first (given) name. |
|
||||
| lastname | sn | Contains the user's last (surname) name. |
|
||||
| phone | telephoneNumber / mobile | Holds the user's phone or mobile number. |
|
||||
| department | departmentNumber / ou | Specifies the department or organizational unit of the user. |
|
||||
| memberof | memberOf | Lists the groups and roles to which the user belongs. |
|
||||
|
||||
#### `login_filter`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** An LDAP filter to restrict which users can log in. Use `{{login_identifier}}` to insert the username.
|
||||
For example:
|
||||
```text
|
||||
(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||
```
|
||||
|
||||
#### `admin_group`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A specific LDAP group whose members are considered administrators in WireGuard Portal.
|
||||
For example:
|
||||
```text
|
||||
CN=WireGuardAdmins,OU=Some-OU,DC=YOURDOMAIN,DC=LOCAL
|
||||
```
|
||||
|
||||
#### `sync_interval`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** How frequently (in duration, e.g. `30m`) to synchronize users from LDAP. Empty or `0` disables sync. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
Only users that match the `sync_filter` are synchronized, if `disable_missing` is `true`, users not found in LDAP are disabled.
|
||||
|
||||
#### `sync_filter`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** An LDAP filter to select which users get synchronized into WireGuard Portal.
|
||||
For example:
|
||||
```text
|
||||
(&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||
```
|
||||
|
||||
#### `disable_missing`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, any user **not** found in LDAP (during sync) is disabled in WireGuard Portal.
|
||||
|
||||
#### `auto_re_enable`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, users that where disabled because they were missing (see `disable_missing`) will be re-enabled once they are found again.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, new user accounts are created in WireGuard Portal upon first login.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, logs LDAP user data at the trace level upon login.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
34
docs/documentation/getting-started/binaries.md
Normal file
34
docs/documentation/getting-started/binaries.md
Normal 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.
|
@@ -1,11 +0,0 @@
|
||||
To build a standalone application, use the Makefile provided in the repository.
|
||||
Go version **1.22** 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
|
||||
```
|
@@ -5,20 +5,7 @@ The preferred way to start WireGuard Portal as Docker container is to use Docker
|
||||
A sample docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
version: '3.6'
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:v2
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
network_mode: "host"
|
||||
ports:
|
||||
- "8888:8888"
|
||||
volumes:
|
||||
- /etc/wireguard:/etc/wireguard
|
||||
- ./data:/app/data
|
||||
- ./config:/app/config
|
||||
--8<-- "docker-compose.yml::17"
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
#### Semantic versioned tags
|
||||
|
||||
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).
|
||||
@@ -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**.
|
||||
|
||||
#### latest
|
||||
|
||||
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.
|
||||
|
||||
#### Branch tags
|
||||
|
||||
For each commit in the master and the stable branch, a corresponding Docker image is build. These images use the `master` or `stable` tags.
|
||||
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
You can configure WireGuard Portal using a yaml configuration file.
|
||||
The filepath of the yaml configuration file defaults to `/app/config/config.yml`.
|
||||
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
|
||||
@@ -61,21 +50,8 @@ 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`.
|
||||
|
||||
You should mount those directories as a volume:
|
||||
|
||||
- /app/data
|
||||
- /app/config
|
||||
|
||||
### Configuration Options
|
||||
All available YAML configuration options are available [here](https://github.com/h44z/wg-portal#configuration).
|
||||
|
||||
A very basic example:
|
||||
|
||||
```yaml
|
||||
core:
|
||||
admin_user: test@wg-portal.local
|
||||
admin_password: secret
|
||||
|
||||
web:
|
||||
external_url: http://localhost:8888
|
||||
request_logging: true
|
||||
```
|
||||
|
||||
A detailed description of the configuration options can be found [here](../configuration/overview.md).
|
||||
|
1
docs/documentation/getting-started/helm.md
Normal file
1
docs/documentation/getting-started/helm.md
Normal file
@@ -0,0 +1 @@
|
||||
--8<-- "./deploy/helm/README.md:16"
|
24
docs/documentation/getting-started/sources.md
Normal file
24
docs/documentation/getting-started/sources.md
Normal 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.
|
32
docs/documentation/monitoring/prometheus.md
Normal file
32
docs/documentation/monitoring/prometheus.md
Normal 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.
|
||||
|
||||

|
@@ -1,29 +1 @@
|
||||
**WireGuard Portal** is a simple, web based configuration portal for [WireGuard](https://wireguard.com).
|
||||
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
|
||||
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~~ (coming soon)
|
||||
|
||||
## Quick-Start
|
||||
|
||||
The easiest way to get started is to use the provided [Docker image](./getting-started/docker.md).
|
||||
|
||||
--8<-- "README.md:20:47"
|
||||
|
1
docs/documentation/rest-api/api-doc.md
Normal file
1
docs/documentation/rest-api/api-doc.md
Normal file
@@ -0,0 +1 @@
|
||||
<swagger-ui src="./swagger.yaml"/>
|
1543
docs/documentation/rest-api/swagger.yaml
Normal file
1543
docs/documentation/rest-api/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
## Upgrade from v1 to v2
|
||||
@@ -18,8 +18,19 @@ You can also specify the database type using the parameter **-migrateFromType**,
|
||||
For example:
|
||||
|
||||
```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.
|
||||
Ensure that the new database does not contain any data!
|
||||
Ensure that the new database does not contain any data!
|
||||
|
||||
If you are using Docker, you can adapt the docker-compose.yml file to start the upgrade process:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:latest
|
||||
# ... other settings
|
||||
restart: no
|
||||
command: ["-migrateFrom=/app/data/wg_portal.db"]
|
||||
```
|
618
frontend/package-lock.json
generated
618
frontend/package-lock.json
generated
@@ -8,26 +8,28 @@
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@kyvg/vue3-notification": "^3.1.3",
|
||||
"@fontsource/nunito-sans": "^5.1.1",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@kyvg/vue3-notification": "^3.4.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootswatch": "^5.3.2",
|
||||
"flag-icons": "^7.1.0",
|
||||
"ip-address": "^9.0.5",
|
||||
"is-cidr": "^5.0.3",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootswatch": "^5.3.3",
|
||||
"flag-icons": "^7.3.2",
|
||||
"ip-address": "^10.0.1",
|
||||
"is-cidr": "^5.1.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia": "^2.3.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"vue": "^3.3.13",
|
||||
"vue-i18n": "^9.14.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.0.1",
|
||||
"vue-prism-component": "github:h44z/vue-prism-component",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue3-tags-input": "^1.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"vite": "^5.0.10"
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"sass-embedded": "^1.83.4",
|
||||
"vite": "^5.4.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
@@ -76,6 +78,13 @@
|
||||
"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": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
@@ -467,23 +476,29 @@
|
||||
"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": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.1.tgz",
|
||||
"integrity": "sha512-ALIk/MOh5gYe1TG/ieS5mVUsk7VUIJTJKPMK9rFFqOgfp0Q3d5QiBXbcOMwUvs37fyZVCz46YjOE6IFeOAXCHA==",
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
|
||||
"integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "9.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.2.tgz",
|
||||
"integrity": "sha512-DZyQ4Hk22sC81MP4qiCDuU+LdaYW91A6lCjq8AWPvY3+mGMzhGDfOCzvyR6YBQxtlPjFqMoFk9ylnNYRAQwXtQ==",
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.0.1.tgz",
|
||||
"integrity": "sha512-NAmhw1l/llM0HZRpagR/ChJTNymW4ll6/4EDSJML5c8L5Hl/+k6UyF8EIgE6DeHpfheQujkSRngauViHqq6jJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "9.14.2",
|
||||
"@intlify/shared": "9.14.2"
|
||||
"@intlify/message-compiler": "11.0.1",
|
||||
"@intlify/shared": "11.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -493,12 +508,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "9.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.2.tgz",
|
||||
"integrity": "sha512-YsKKuV4Qv4wrLNsvgWbTf0E40uRv+Qiw1BeLQ0LAxifQuhiMe+hfTIzOMdWj/ZpnTDj4RSZtkXjJM7JDiiB5LQ==",
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.0.1.tgz",
|
||||
"integrity": "sha512-5RFH8x+Mn3mbjcHXnb6KCXGiczBdiQkWkv99iiA0JpKrNuTAQeW59Pjq/uObMB0eR0shnKYGTkIJxum+DbL3sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "9.14.2",
|
||||
"@intlify/shared": "11.0.1",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -509,9 +524,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "9.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.2.tgz",
|
||||
"integrity": "sha512-uRAHAxYPeF+G5DBIboKpPgC/Waecd4Jz8ihtkpJQD5ycb5PwXp0k/+hBGl5dAjwF7w+l74kz/PKA8r8OK//RUw==",
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.0.1.tgz",
|
||||
"integrity": "sha512-lH164+aDDptHZ3dBDbIhRa1dOPQUp+83iugpc+1upTOWCnwyC1PVis6rSWNMMJ8VQxvtHQB9JMib48K55y0PvQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -805,16 +820,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
|
||||
"integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz",
|
||||
"integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4.0.0 || ^5.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
@@ -949,6 +964,13 @@
|
||||
"integrity": "sha512-cJLhobnZsVCelU7zdH/L7wpcXAyUoTX4/5l2dWQ0JXgaVK80BdTQNU/ImWwoyIGBeyms4iQDAdNtOfPQZf0Atg==",
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-4.1.1.tgz",
|
||||
@@ -985,6 +1007,13 @@
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz",
|
||||
@@ -1061,9 +1090,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/flag-icons": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.2.3.tgz",
|
||||
"integrity": "sha512-X2gUdteNuqdNqob2KKTJTS+ZCvyWeLCtDz9Ty8uJP17Y4o82Y+U/Vd4JNrdwTAjagYsRznOn9DZ+E/Q52qbmqg==",
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.3.2.tgz",
|
||||
"integrity": "sha512-QkaZ6Zvai8LIjx+UNAHUJ5Dhz9OLZpBDwCRWxF6YErxIcR16jTkIFm3bFu54EkvKQy4+wicW+Gm7/0631wVQyQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
@@ -1093,15 +1122,28 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
@@ -1158,12 +1200,6 @@
|
||||
"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": {
|
||||
"version": "0.30.14",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz",
|
||||
@@ -1198,9 +1234,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.6.tgz",
|
||||
"integrity": "sha512-vIsR8JkDN5Ga2vAxqOE2cJj4VtsHnzpR1Fz30kClxlh0yCHfec6uoMeM3e/ddqmwFUejK3NlrcQa/shnpyT4hA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
|
||||
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.3",
|
||||
@@ -1210,14 +1246,10 @@
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.4.0",
|
||||
"typescript": ">=4.4.4",
|
||||
"vue": "^2.6.14 || ^3.5.11"
|
||||
"vue": "^2.7.0 || ^3.5.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
@@ -1324,6 +1356,401 @@
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1333,12 +1760,6 @@
|
||||
"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": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz",
|
||||
@@ -1356,6 +1777,45 @@
|
||||
"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": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz",
|
||||
@@ -1371,10 +1831,24 @@
|
||||
"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": {
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
|
||||
"version": "5.4.14",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
|
||||
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1453,13 +1927,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "9.14.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.2.tgz",
|
||||
"integrity": "sha512-JK9Pm80OqssGJU2Y6F7DcM8RFHqVG4WkuCqOZTVsXkEzZME7ABejAUqUdA931zEBedc4thBgSUWxeQh4uocJAQ==",
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.0.1.tgz",
|
||||
"integrity": "sha512-pWAT8CusK8q9/EpN7V3oxwHwxWm6+Kp2PeTZmRGvdZTkUzMQDpbbmHp0TwQ8xw04XKm23cr6B4GL72y3W8Yekg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "9.14.2",
|
||||
"@intlify/shared": "9.14.2",
|
||||
"@intlify/core-base": "11.0.1",
|
||||
"@intlify/shared": "11.0.1",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@@ -8,25 +8,27 @@
|
||||
"preview": "vite preview --port 5050"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@kyvg/vue3-notification": "^3.1.3",
|
||||
"@fontsource/nunito-sans": "^5.1.1",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@kyvg/vue3-notification": "^3.4.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootswatch": "^5.3.2",
|
||||
"flag-icons": "^7.1.0",
|
||||
"ip-address": "^9.0.5",
|
||||
"is-cidr": "^5.0.3",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootswatch": "^5.3.3",
|
||||
"flag-icons": "^7.3.2",
|
||||
"ip-address": "^10.0.1",
|
||||
"is-cidr": "^5.1.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia": "^2.3.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"vue": "^3.3.13",
|
||||
"vue-i18n": "^9.14.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.0.1",
|
||||
"vue-prism-component": "github:h44z/vue-prism-component",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue3-tags-input": "^1.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"vite": "^5.0.10"
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"sass-embedded": "^1.83.4",
|
||||
"vite": "^5.4.12"
|
||||
}
|
||||
}
|
||||
|
@@ -45,13 +45,12 @@ const languageFlag = computed(() => {
|
||||
if (!appGlobal.$i18n.availableLocales.includes(lang)) {
|
||||
lang = appGlobal.$i18n.fallbackLocale;
|
||||
}
|
||||
if (lang === "en") {
|
||||
lang = "us";
|
||||
}
|
||||
if (lang === "zh") {
|
||||
lang = "cn";
|
||||
}
|
||||
return "fi-" + lang;
|
||||
const langMap = {
|
||||
en: "us",
|
||||
uk: "ua",
|
||||
zh: "cn",
|
||||
};
|
||||
return "fi-" + (langMap[lang] || lang);
|
||||
})
|
||||
|
||||
const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
||||
@@ -90,6 +89,7 @@ const currentYear = ref(new Date().getFullYear())
|
||||
href="#" role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
||||
<div class="dropdown-menu">
|
||||
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
||||
<RouterLink :to="{ name: 'settings' }" class="dropdown-item" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</RouterLink>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" @click.prevent="auth.Logout"><i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}</a>
|
||||
</div>
|
||||
@@ -116,9 +116,11 @@ const currentYear = ref(new Date().getFullYear())
|
||||
<button aria-expanded="false" aria-haspopup="true" class="btn btn btn-secondary pe-0"
|
||||
data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button>
|
||||
<div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style="">
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('fr')"><span class="fi fi-fr"></span> Français</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ru')"><span class="fi fi-ru"></span> Русский</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('uk')"><span class="fi fi-ua"></span> Українська</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('vi')"><span class="fi fi-vi"></span> Tiếng Việt</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('zh')"><span class="fi fi-cn"></span> 中文</a>
|
||||
</div>
|
||||
|
16
frontend/src/assets/custom.scss
Normal file
16
frontend/src/assets/custom.scss
Normal 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;
|
||||
*/
|
@@ -165,7 +165,7 @@ async function del() {
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.userId!=='#NEW#'&&formData.Source==='db'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $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>
|
||||
|
294
frontend/src/components/UserPeerEditModal.vue
Normal file
294
frontend/src/components/UserPeerEditModal.vue
Normal 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>
|
@@ -88,6 +88,10 @@ function close() {
|
||||
<td>{{ $t('modals.user-view.department') }}:</td>
|
||||
<td>{{selectedUser.Department}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.api-enabled') }}:</td>
|
||||
<td>{{selectedUser.ApiEnabled}}</td>
|
||||
</tr>
|
||||
<tr v-if="selectedUser.Disabled">
|
||||
<td>{{ $t('modals.user-view.disabled') }}:</td>
|
||||
<td>{{selectedUser.DisabledReason}}</td>
|
||||
|
@@ -146,6 +146,8 @@ export function freshUser() {
|
||||
Locked: false,
|
||||
LockedReason: "",
|
||||
|
||||
ApiEnabled: false,
|
||||
|
||||
PeerCount: 0
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,9 @@
|
||||
// src/lang/index.js
|
||||
import de from './translations/de.json';
|
||||
import ru from './translations/ru.json';
|
||||
import en from './translations/en.json';
|
||||
import fr from './translations/fr.json';
|
||||
import ru from './translations/ru.json';
|
||||
import uk from './translations/uk.json';
|
||||
import vi from './translations/vi.json';
|
||||
import zh from './translations/zh.json';
|
||||
import {createI18n} from "vue-i18n";
|
||||
@@ -19,10 +21,12 @@ const i18n = createI18n({
|
||||
fallbackLocale: "en", // set fallback locale
|
||||
messages: {
|
||||
"de": de,
|
||||
"ru": ru,
|
||||
"en": en,
|
||||
"fr": fr,
|
||||
"ru": ru,
|
||||
"uk": uk,
|
||||
"vi": vi,
|
||||
"zh": zh
|
||||
"zh": zh,
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -37,6 +37,7 @@
|
||||
"users": "Benutzer",
|
||||
"lang": "Sprache ändern",
|
||||
"profile": "Mein Profil",
|
||||
"settings": "Einstellungen",
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden"
|
||||
},
|
||||
@@ -167,6 +168,26 @@
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer"
|
||||
},
|
||||
"settings": {
|
||||
"headline": "Einstellungen",
|
||||
"abstract": "Hier finden Sie persönliche Einstellungen für WireGuard Portal.",
|
||||
"api": {
|
||||
"headline": "API Einstellungen",
|
||||
"abstract": "Hier können Sie die RESTful API verwalten.",
|
||||
"active-description": "Die API ist derzeit für Ihr Benutzerkonto aktiv. Alle API-Anfragen werden mit Basic Auth authentifiziert. Verwenden Sie zur Authentifizierung die folgenden Anmeldeinformationen.",
|
||||
"inactive-description": "Die API ist derzeit inaktiv. Klicken Sie auf die Schaltfläche unten, um sie zu aktivieren.",
|
||||
"user-label": "API Benutzername:",
|
||||
"user-placeholder": "API Benutzer",
|
||||
"token-label": "API Passwort:",
|
||||
"token-placeholder": "API Token",
|
||||
"token-created-label": "API-Zugriff gewährt seit: ",
|
||||
"button-disable-title": "Deaktivieren Sie die API. Dadurch wird der aktuelle Token ungültig.",
|
||||
"button-disable-text": "API deaktivieren",
|
||||
"button-enable-title": "Aktivieren Sie die API, dadurch wird ein neuer Token generiert.",
|
||||
"button-enable-text": "API aktivieren",
|
||||
"api-link": "API Dokumentation"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "User Account:",
|
||||
@@ -333,7 +354,7 @@
|
||||
"endpoint": {
|
||||
"label": "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": {
|
||||
"label": "IP Networks",
|
||||
|
@@ -37,6 +37,7 @@
|
||||
"users": "Users",
|
||||
"lang": "Toggle Language",
|
||||
"profile": "My Profile",
|
||||
"settings": "Settings",
|
||||
"login": "Login",
|
||||
"logout": "Logout"
|
||||
},
|
||||
@@ -167,6 +168,26 @@
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer"
|
||||
},
|
||||
"settings": {
|
||||
"headline": "Settings",
|
||||
"abstract": "Here you can change your personal settings.",
|
||||
"api": {
|
||||
"headline": "API Settings",
|
||||
"abstract": "Here you can configure the RESTful API settings.",
|
||||
"active-description": "The API is currently active for your user account. All API requests are authenticated with Basic Auth. Use the following credentials for authentication.",
|
||||
"inactive-description": "The API is currently inactive. Press the button below to activate it.",
|
||||
"user-label": "API Username:",
|
||||
"user-placeholder": "The API user",
|
||||
"token-label": "API Password:",
|
||||
"token-placeholder": "The API token",
|
||||
"token-created-label": "API access granted at: ",
|
||||
"button-disable-title": "Disable API, this will invalidate the current token.",
|
||||
"button-disable-text": "Disable API",
|
||||
"button-enable-title": "Enable API, this will generate a new token.",
|
||||
"button-enable-text": "Enable API",
|
||||
"api-link": "API Documentation"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "User Account:",
|
||||
@@ -177,8 +198,9 @@
|
||||
"email": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"phone": "Phone number",
|
||||
"phone": "Phone Number",
|
||||
"department": "Department",
|
||||
"api-enabled": "API Access",
|
||||
"disabled": "Account Disabled",
|
||||
"locked": "Account Locked",
|
||||
"no-peers": "User has no associated peers.",
|
||||
@@ -333,7 +355,7 @@
|
||||
"endpoint": {
|
||||
"label": "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": {
|
||||
"label": "IP Networks",
|
||||
|
515
frontend/src/lang/translations/fr.json
Normal file
515
frontend/src/lang/translations/fr.json
Normal file
@@ -0,0 +1,515 @@
|
||||
{
|
||||
"languages": {
|
||||
"fr": "Français"
|
||||
},
|
||||
"general": {
|
||||
"pagination": {
|
||||
"size": "Nombre d'éléments",
|
||||
"all": "Tous (lent)"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Rechercher...",
|
||||
"button": "Rechercher"
|
||||
},
|
||||
"select-all": "Tout sélectionner",
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
"cancel": "Annuler",
|
||||
"close": "Fermer",
|
||||
"save": "Enregistrer",
|
||||
"delete": "Supprimer"
|
||||
},
|
||||
"login": {
|
||||
"headline": "Veuillez vous connecter",
|
||||
"username": {
|
||||
"label": "Nom d'utilisateur",
|
||||
"placeholder": "Veuillez entrer votre nom d'utilisateur"
|
||||
},
|
||||
"password": {
|
||||
"label": "Mot de passe",
|
||||
"placeholder": "Veuillez entrer votre mot de passe"
|
||||
},
|
||||
"button": "Se connecter"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Accueil",
|
||||
"interfaces": "Interfaces",
|
||||
"users": "Utilisateurs",
|
||||
"lang": "Changer de langue",
|
||||
"profile": "Mon profil",
|
||||
"settings": "Paramètres",
|
||||
"login": "Se connecter",
|
||||
"logout": "Se déconnecter"
|
||||
},
|
||||
"home": {
|
||||
"headline": "Portail VPN WireGuard®",
|
||||
"info-headline": "Plus d'informations",
|
||||
"abstract": "WireGuard® est un VPN extrêmement simple mais rapide et moderne qui utilise une cryptographie de pointe. Il vise à être plus rapide, plus simple, plus léger et plus utile qu'IPsec, tout en évitant le casse-tête massif. Il se veut considérablement plus performant qu'OpenVPN.",
|
||||
"installation": {
|
||||
"box-header": "Installation de WireGuard",
|
||||
"headline": "Installation",
|
||||
"content": "Les instructions d'installation du logiciel client sont disponibles sur le site Web officiel de WireGuard.",
|
||||
"button": "Ouvrir les instructions"
|
||||
},
|
||||
"about-wg": {
|
||||
"box-header": "À propos de WireGuard",
|
||||
"headline": "À propos",
|
||||
"content": "WireGuard® est un VPN extrêmement simple mais rapide et moderne qui utilise une cryptographie de pointe.",
|
||||
"button": "Plus d'informations"
|
||||
},
|
||||
"about-portal": {
|
||||
"box-header": "À propos du Portail WireGuard",
|
||||
"headline": "Portail WireGuard",
|
||||
"content": "Le Portail WireGuard est un portail de configuration simple basé sur le Web pour WireGuard.",
|
||||
"button": "Plus d'informations"
|
||||
},
|
||||
"profiles": {
|
||||
"headline": "Profils VPN",
|
||||
"abstract": "Vous pouvez accéder et télécharger vos configurations VPN personnelles via votre profil utilisateur.",
|
||||
"content": "Pour trouver tous vos profils configurés, cliquez sur le bouton ci-dessous.",
|
||||
"button": "Ouvrir mon profil"
|
||||
},
|
||||
"admin": {
|
||||
"headline": "Zone d'administration",
|
||||
"abstract": "Dans la zone d'administration, vous pouvez gérer les pairs WireGuard et l'interface du serveur, ainsi que les utilisateurs autorisés à se connecter au Portail WireGuard.",
|
||||
"content": "",
|
||||
"button-admin": "Ouvrir l'administration du serveur",
|
||||
"button-user": "Ouvrir l'administration des utilisateurs"
|
||||
}
|
||||
},
|
||||
"interfaces": {
|
||||
"headline": "Administration des interfaces",
|
||||
"headline-peers": "Pairs VPN actuels",
|
||||
"headline-endpoints": "Points de terminaison actuels",
|
||||
"no-interface": {
|
||||
"default-selection": "Aucune interface disponible",
|
||||
"headline": "Aucune interface trouvée...",
|
||||
"abstract": "Cliquez sur le bouton plus ci-dessus pour créer une nouvelle interface WireGuard."
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "Aucun pair disponible",
|
||||
"abstract": "Actuellement, aucun pair n'est disponible pour l'interface WireGuard sélectionnée."
|
||||
},
|
||||
"table-heading": {
|
||||
"name": "Nom",
|
||||
"user": "Utilisateur",
|
||||
"ip": "IP",
|
||||
"endpoint": "Point de terminaison",
|
||||
"status": "Statut"
|
||||
},
|
||||
"interface": {
|
||||
"headline": "État de l'interface pour",
|
||||
"mode": "mode",
|
||||
"key": "Clé publique",
|
||||
"endpoint": "Point de terminaison public",
|
||||
"port": "Port d'écoute",
|
||||
"peers": "Pairs activés",
|
||||
"total-peers": "Total des pairs",
|
||||
"endpoints": "Points de terminaison activés",
|
||||
"total-endpoints": "Total des points de terminaison",
|
||||
"ip": "Adresse IP",
|
||||
"default-allowed-ip": "IP autorisées par défaut",
|
||||
"dns": "Serveurs DNS",
|
||||
"mtu": "MTU",
|
||||
"default-keep-alive": "Intervalle Keepalive par défaut",
|
||||
"button-show-config": "Afficher la configuration",
|
||||
"button-download-config": "Télécharger la configuration",
|
||||
"button-store-config": "Enregistrer la configuration pour wg-quick",
|
||||
"button-edit": "Modifier l'interface"
|
||||
},
|
||||
"button-add-interface": "Ajouter une interface",
|
||||
"button-add-peer": "Ajouter un pair",
|
||||
"button-add-peers": "Ajouter plusieurs pairs",
|
||||
"button-show-peer": "Afficher le pair",
|
||||
"button-edit-peer": "Modifier le pair",
|
||||
"peer-disabled": "Le pair est désactivé, raison :",
|
||||
"peer-expiring": "Le pair expire le",
|
||||
"peer-connected": "Connecté",
|
||||
"peer-not-connected": "Non connecté",
|
||||
"peer-handshake": "Dernière négociation :",
|
||||
"button-show-peer": "Afficher le pair",
|
||||
"button-edit-peer": "Modifier le pair"
|
||||
},
|
||||
"users": {
|
||||
"headline": "Administration des utilisateurs",
|
||||
"table-heading": {
|
||||
"id": "ID",
|
||||
"email": "E-mail",
|
||||
"firstname": "Prénom",
|
||||
"lastname": "Nom",
|
||||
"source": "Source",
|
||||
"peers": "Pairs",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"no-user": {
|
||||
"headline": "Aucun utilisateur disponible",
|
||||
"abstract": "Actuellement, aucun utilisateur n'est enregistré auprès du Portail WireGuard."
|
||||
},
|
||||
"button-add-user": "Ajouter un utilisateur",
|
||||
"button-show-user": "Afficher l'utilisateur",
|
||||
"button-edit-user": "Modifier l'utilisateur",
|
||||
"user-disabled": "L'utilisateur est désactivé, raison :",
|
||||
"user-locked": "Le compte est verrouillé, raison :",
|
||||
"admin": "L'utilisateur a des privilèges d'administrateur",
|
||||
"no-admin": "L'utilisateur n'a pas de privilèges d'administrateur"
|
||||
},
|
||||
"profile": {
|
||||
"headline": "Mes pairs VPN",
|
||||
"table-heading": {
|
||||
"name": "Nom",
|
||||
"ip": "IP",
|
||||
"stats": "Statut",
|
||||
"interface": "Interface serveur"
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "Aucun pair disponible",
|
||||
"abstract": "Actuellement, aucun pair n'est associé à votre profil utilisateur."
|
||||
},
|
||||
"peer-connected": "Connecté",
|
||||
"button-add-peer": "Ajouter un pair",
|
||||
"button-show-peer": "Afficher le pair",
|
||||
"button-edit-peer": "Modifier le pair"
|
||||
},
|
||||
"settings": {
|
||||
"headline": "Paramètres",
|
||||
"abstract": "Ici, vous pouvez modifier vos paramètres personnels.",
|
||||
"api": {
|
||||
"headline": "Paramètres de l'API",
|
||||
"abstract": "Ici, vous pouvez configurer les paramètres de l'API RESTful.",
|
||||
"active-description": "L'API est actuellement active pour votre compte utilisateur. Toutes les requêtes API sont authentifiées avec l'authentification de base. Utilisez les informations d'identification suivantes pour l'authentification.",
|
||||
"inactive-description": "L'API est actuellement inactive. Appuyez sur le bouton ci-dessous pour l'activer.",
|
||||
"user-label": "Nom d'utilisateur de l'API :",
|
||||
"user-placeholder": "L'utilisateur de l'API",
|
||||
"token-label": "Mot de passe de l'API :",
|
||||
"token-placeholder": "Le jeton de l'API",
|
||||
"token-created-label": "Accès API accordé le :",
|
||||
"button-disable-title": "Désactiver l'API, cela invalidera le jeton actuel.",
|
||||
"button-disable-text": "Désactiver l'API",
|
||||
"button-enable-title": "Activer l'API, cela générera un nouveau jeton.",
|
||||
"button-enable-text": "Activer l'API",
|
||||
"api-link": "Documentation de l'API"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "Compte utilisateur :",
|
||||
"tab-user": "Informations",
|
||||
"tab-peers": "Pairs",
|
||||
"headline-info": "Informations sur l'utilisateur :",
|
||||
"headline-notes": "Notes :",
|
||||
"email": "E-mail",
|
||||
"firstname": "Prénom",
|
||||
"lastname": "Nom",
|
||||
"phone": "Numéro de téléphone",
|
||||
"department": "Département",
|
||||
"api-enabled": "Accès API",
|
||||
"disabled": "Compte désactivé",
|
||||
"locked": "Compte verrouillé",
|
||||
"no-peers": "L'utilisateur n'a pas de pairs associés.",
|
||||
"peers": {
|
||||
"name": "Nom",
|
||||
"interface": "Interface",
|
||||
"ip": "IP"
|
||||
}
|
||||
},
|
||||
"user-edit": {
|
||||
"headline-edit": "Modifier l'utilisateur :",
|
||||
"headline-new": "Nouvel utilisateur",
|
||||
"header-general": "Général",
|
||||
"header-personal": "Informations sur l'utilisateur",
|
||||
"header-notes": "Notes",
|
||||
"header-state": "État",
|
||||
"identifier": {
|
||||
"label": "Identifiant",
|
||||
"placeholder": "L'identifiant unique de l'utilisateur"
|
||||
},
|
||||
"source": {
|
||||
"label": "Source",
|
||||
"placeholder": "La source de l'utilisateur"
|
||||
},
|
||||
"password": {
|
||||
"label": "Mot de passe",
|
||||
"placeholder": "Un mot de passe super secret",
|
||||
"description": "Laissez ce champ vide pour conserver le mot de passe actuel."
|
||||
},
|
||||
"email": {
|
||||
"label": "E-mail",
|
||||
"placeholder": "L'adresse e-mail"
|
||||
},
|
||||
"phone": {
|
||||
"label": "Téléphone",
|
||||
"placeholder": "Le numéro de téléphone"
|
||||
},
|
||||
"department": {
|
||||
"label": "Département",
|
||||
"placeholder": "Le département"
|
||||
},
|
||||
"firstname": {
|
||||
"label": "Prénom",
|
||||
"placeholder": "Prénom"
|
||||
},
|
||||
"lastname": {
|
||||
"label": "Nom",
|
||||
"placeholder": "Nom"
|
||||
},
|
||||
"notes": {
|
||||
"label": "Notes",
|
||||
"placeholder": ""
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Désactivé (aucune connexion WireGuard et aucune connexion possible)"
|
||||
},
|
||||
"locked": {
|
||||
"label": "Verrouillé (aucune connexion possible, les connexions WireGuard fonctionnent toujours)"
|
||||
},
|
||||
"admin": {
|
||||
"label": "Est Admin"
|
||||
}
|
||||
},
|
||||
"interface-view": {
|
||||
"headline": "Configuration pour l'interface :"
|
||||
},
|
||||
"interface-edit": {
|
||||
"headline-edit": "Modifier l'interface :",
|
||||
"headline-new": "Nouvelle interface",
|
||||
"tab-interface": "Interface",
|
||||
"tab-peerdef": "Valeurs par défaut des pairs",
|
||||
"header-general": "Général",
|
||||
"header-network": "Réseau",
|
||||
"header-crypto": "Cryptographie",
|
||||
"header-hooks": "Hooks d'interface",
|
||||
"header-peer-hooks": "Hooks",
|
||||
"header-state": "État",
|
||||
"identifier": {
|
||||
"label": "Identifiant",
|
||||
"placeholder": "L'identifiant unique de l'interface"
|
||||
},
|
||||
"mode": {
|
||||
"label": "Mode de l'interface",
|
||||
"server": "Mode serveur",
|
||||
"client": "Mode client",
|
||||
"any": "Mode inconnu"
|
||||
},
|
||||
"display-name": {
|
||||
"label": "Nom d'affichage",
|
||||
"placeholder": "Le nom descriptif de l'interface"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Clé privée",
|
||||
"placeholder": "La clé privée"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Clé publique",
|
||||
"placeholder": "La clé publique"
|
||||
},
|
||||
"ip": {
|
||||
"label": "Adresses IP",
|
||||
"placeholder": "Adresses IP (format CIDR)"
|
||||
},
|
||||
"listen-port": {
|
||||
"label": "Port d'écoute",
|
||||
"placeholder": "Le port d'écoute"
|
||||
},
|
||||
"dns": {
|
||||
"label": "Serveur DNS",
|
||||
"placeholder": "Les serveurs DNS qui doivent être utilisés"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "Domaines de recherche DNS",
|
||||
"placeholder": "Préfixes de recherche DNS"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "Le MTU de l'interface (0 = conserver la valeur par défaut)"
|
||||
},
|
||||
"firewall-mark": {
|
||||
"label": "Marque de pare-feu",
|
||||
"placeholder": "Marque de pare-feu appliquée au trafic sortant. (0 = automatique)"
|
||||
},
|
||||
"routing-table": {
|
||||
"label": "Table de routage",
|
||||
"placeholder": "L'ID de la table de routage",
|
||||
"description": "Cas particuliers : off = ne pas gérer les routes, 0 = automatique"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pré-Up",
|
||||
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pré-Down",
|
||||
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Interface désactivée"
|
||||
},
|
||||
"save-config": {
|
||||
"label": "Enregistrer automatiquement la configuration wg-quick"
|
||||
},
|
||||
"defaults": {
|
||||
"endpoint": {
|
||||
"label": "Adresse du point de terminaison",
|
||||
"placeholder": "Adresse du point de terminaison",
|
||||
"description": "L'adresse du point de terminaison auquel les pairs se connecteront. (par exemple, wg.example.com ou wg.example.com:51820)"
|
||||
},
|
||||
"networks": {
|
||||
"label": "Réseaux IP",
|
||||
"placeholder": "Adresses de réseau",
|
||||
"description": "Les pairs recevront des adresses IP de ces sous-réseaux."
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Adresses IP autorisées",
|
||||
"placeholder": "Adresses IP autorisées par défaut"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "Le MTU du client (0 = conserver la valeur par défaut)"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Intervalle Keep Alive",
|
||||
"placeholder": "Persistent Keepalive (0 = par défaut)"
|
||||
}
|
||||
},
|
||||
"button-apply-defaults": "Appliquer les valeurs par défaut des pairs"
|
||||
},
|
||||
"peer-view": {
|
||||
"headline-peer": "Pair :",
|
||||
"headline-endpoint": "Point de terminaison :",
|
||||
"section-info": "Informations sur le pair",
|
||||
"section-status": "État actuel",
|
||||
"section-config": "Configuration",
|
||||
"identifier": "Identifiant",
|
||||
"ip": "Adresses IP",
|
||||
"user": "Utilisateur associé",
|
||||
"notes": "Notes",
|
||||
"expiry-status": "Expire le",
|
||||
"disabled-status": "Désactivé le",
|
||||
"traffic": "Trafic",
|
||||
"connection-status": "Statistiques de connexion",
|
||||
"upload": "Octets envoyés (du serveur au pair)",
|
||||
"download": "Octets téléchargés (du pair au serveur)",
|
||||
"pingable": "Peut être pingé",
|
||||
"handshake": "Dernière négociation",
|
||||
"connected-since": "Connecté depuis",
|
||||
"endpoint": "Point de terminaison",
|
||||
"button-download": "Télécharger la configuration",
|
||||
"button-email": "Envoyer la configuration par e-mail"
|
||||
},
|
||||
"peer-edit": {
|
||||
"headline-edit-peer": "Modifier le pair :",
|
||||
"headline-edit-endpoint": "Modifier le point de terminaison :",
|
||||
"headline-new-peer": "Créer un pair",
|
||||
"headline-new-endpoint": "Créer un point de terminaison",
|
||||
"header-general": "Général",
|
||||
"header-network": "Réseau",
|
||||
"header-crypto": "Cryptographie",
|
||||
"header-hooks": "Hooks (exécutés sur le pair)",
|
||||
"header-state": "État",
|
||||
"display-name": {
|
||||
"label": "Nom d'affichage",
|
||||
"placeholder": "Le nom descriptif du pair"
|
||||
},
|
||||
"linked-user": {
|
||||
"label": "Utilisateur lié",
|
||||
"placeholder": "Le compte utilisateur qui possède ce pair"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Clé privée",
|
||||
"placeholder": "La clé privée"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Clé publique",
|
||||
"placeholder": "La clé publique"
|
||||
},
|
||||
"preshared-key": {
|
||||
"label": "Clé pré-partagée",
|
||||
"placeholder": "Clé pré-partagée facultative"
|
||||
},
|
||||
"endpoint-public-key": {
|
||||
"label": "Clé publique du point de terminaison",
|
||||
"placeholder": "La clé publique du point de terminaison distant"
|
||||
},
|
||||
"endpoint": {
|
||||
"label": "Adresse du point de terminaison",
|
||||
"placeholder": "L'adresse du point de terminaison distant"
|
||||
},
|
||||
"ip": {
|
||||
"label": "Adresses IP",
|
||||
"placeholder": "Adresses IP (format CIDR)"
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Adresses IP autorisées",
|
||||
"placeholder": "Adresses IP autorisées (format CIDR)"
|
||||
},
|
||||
"extra-allowed-ip": {
|
||||
"label": "Adresses IP autorisées supplémentaires",
|
||||
"placeholder": "IP autorisées supplémentaires (côté serveur)",
|
||||
"description": "Ces IP seront ajoutées à l'interface WireGuard distante comme IP autorisées."
|
||||
},
|
||||
"dns": {
|
||||
"label": "Serveur DNS",
|
||||
"placeholder": "Les serveurs DNS qui doivent être utilisés"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "Domaines de recherche DNS",
|
||||
"placeholder": "Préfixes de recherche DNS"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Intervalle Keep Alive",
|
||||
"placeholder": "Persistent Keepalive (0 = par défaut)"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "Le MTU du client (0 = conserver la valeur par défaut)"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pré-Up",
|
||||
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pré-Down",
|
||||
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "Une ou plusieurs commandes bash séparées par ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Pair désactivé"
|
||||
},
|
||||
"ignore-global": {
|
||||
"label": "Ignorer les paramètres globaux"
|
||||
},
|
||||
"expires-at": {
|
||||
"label": "Date d'expiration"
|
||||
}
|
||||
},
|
||||
"peer-multi-create": {
|
||||
"headline-peer": "Créer plusieurs pairs",
|
||||
"headline-endpoint": "Créer plusieurs points de terminaison",
|
||||
"identifiers": {
|
||||
"label": "Identifiants d'utilisateur",
|
||||
"placeholder": "Identifiants d'utilisateur",
|
||||
"description": "Un identifiant d'utilisateur (le nom d'utilisateur) pour lequel un pair doit être créé."
|
||||
},
|
||||
"prefix": {
|
||||
"headline-peer": "Pair :",
|
||||
"headline-endpoint": "Point de terminaison :",
|
||||
"label": "Préfixe du nom d'affichage",
|
||||
"placeholder": "Le préfixe",
|
||||
"description": "Un préfixe qui est ajouté au nom d'affichage des pairs."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
515
frontend/src/lang/translations/uk.json
Normal file
515
frontend/src/lang/translations/uk.json
Normal file
@@ -0,0 +1,515 @@
|
||||
{
|
||||
"languages": {
|
||||
"uk": "Українська"
|
||||
},
|
||||
"general": {
|
||||
"pagination": {
|
||||
"size": "Кількість елементів",
|
||||
"all": "Всі (повільно)"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Пошук...",
|
||||
"button": "Пошук"
|
||||
},
|
||||
"select-all": "Вибрати все",
|
||||
"yes": "Так",
|
||||
"no": "Ні",
|
||||
"cancel": "Скасувати",
|
||||
"close": "Закрити",
|
||||
"save": "Зберегти",
|
||||
"delete": "Видалити"
|
||||
},
|
||||
"login": {
|
||||
"headline": "Будь ласка, увійдіть",
|
||||
"username": {
|
||||
"label": "Ім'я користувача",
|
||||
"placeholder": "Введіть ім'я користувача"
|
||||
},
|
||||
"password": {
|
||||
"label": "Пароль",
|
||||
"placeholder": "Введіть пароль"
|
||||
},
|
||||
"button": "Увійти"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Головна",
|
||||
"interfaces": "Інтерфейси",
|
||||
"users": "Користувачі",
|
||||
"lang": "Змінити мову",
|
||||
"profile": "Мій профіль",
|
||||
"settings": "Налаштування",
|
||||
"login": "Вхід",
|
||||
"logout": "Вийти"
|
||||
},
|
||||
"home": {
|
||||
"headline": "WireGuard® VPN Портал",
|
||||
"info-headline": "Додаткова інформація",
|
||||
"abstract": "WireGuard® — це високоефективний, сучасний і легкий VPN, який використовує передову криптографію. Розроблений для простоти та швидкості, він перевершує IPsec, усуваючи зайву складність. Крім того, він прагне забезпечити значно кращу продуктивність, ніж OpenVPN.",
|
||||
"installation": {
|
||||
"box-header": "Встановлення WireGuard",
|
||||
"headline": "Встановлення",
|
||||
"content": "Інструкції щодо встановлення клієнтського програмного забезпечення можна знайти на офіційному сайті WireGuard.",
|
||||
"button": "Відкрити інструкції"
|
||||
},
|
||||
"about-wg": {
|
||||
"box-header": "Про WireGuard",
|
||||
"headline": "Про програму",
|
||||
"content": "WireGuard® — це надзвичайно простий, швидкий і сучасний VPN, що використовує передову криптографію.",
|
||||
"button": "Докладніше"
|
||||
},
|
||||
"about-portal": {
|
||||
"box-header": "Про портал WireGuard",
|
||||
"headline": "Портал WireGuard",
|
||||
"content": "Портал WireGuard — це простий веб-інтерфейс для налаштування WireGuard.",
|
||||
"button": "Докладніше"
|
||||
},
|
||||
"profiles": {
|
||||
"headline": "VPN профілі",
|
||||
"abstract": "Ви можете отримати доступ та завантажити свої особисті VPN-конфігурації через свій профіль користувача.",
|
||||
"content": "Щоб переглянути всі налаштовані профілі, натисніть кнопку нижче.",
|
||||
"button": "Відкрити мій профіль"
|
||||
},
|
||||
"admin": {
|
||||
"headline": "Адміністративна панель",
|
||||
"abstract": "У адміністративній панелі ви можете керувати клієнтами WireGuard, серверним інтерфейсом і користувачами, які мають доступ до порталу WireGuard.",
|
||||
"content": "",
|
||||
"button-admin": "Відкрити адміністрування сервера",
|
||||
"button-user": "Відкрити адміністрування користувачів"
|
||||
}
|
||||
},
|
||||
"interfaces": {
|
||||
"headline": "Адміністрування інтерфейсів",
|
||||
"headline-peers": "Поточні VPN-піри",
|
||||
"headline-endpoints": "Поточні кінцеві точки",
|
||||
"no-interface": {
|
||||
"default-selection": "Немає доступного інтерфейсу",
|
||||
"headline": "Інтерфейси не знайдено...",
|
||||
"abstract": "Натисніть кнопку з плюсом вище, щоб створити новий інтерфейс WireGuard."
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "Немає доступних пірів",
|
||||
"abstract": "Наразі немає доступних пірів для вибраного інтерфейсу WireGuard."
|
||||
},
|
||||
"table-heading": {
|
||||
"name": "Ім'я",
|
||||
"user": "Користувач",
|
||||
"ip": "IP-адреси",
|
||||
"endpoint": "Кінцева точка",
|
||||
"status": "Статус"
|
||||
},
|
||||
"interface": {
|
||||
"headline": "Статус інтерфейсу для",
|
||||
"mode": "режим",
|
||||
"key": "Публічний ключ",
|
||||
"endpoint": "Публічна кінцева точка",
|
||||
"port": "Порт прослуховування",
|
||||
"peers": "Увімкнені піри",
|
||||
"total-peers": "Загальна кількість пірів",
|
||||
"endpoints": "Увімкнені кінцеві точки",
|
||||
"total-endpoints": "Загальна кількість кінцевих точок",
|
||||
"ip": "IP-адреса",
|
||||
"default-allowed-ip": "Типові дозволені IP-адреси",
|
||||
"dns": "DNS-сервери",
|
||||
"mtu": "MTU",
|
||||
"default-keep-alive": "Типовий інтервал Keepalive",
|
||||
"button-show-config": "Показати конфігурацію",
|
||||
"button-download-config": "Завантажити конфігурацію",
|
||||
"button-store-config": "Зберегти конфігурацію для wg-quick",
|
||||
"button-edit": "Редагувати інтерфейс"
|
||||
},
|
||||
"button-add-interface": "Додати інтерфейс",
|
||||
"button-add-peer": "Додати пір",
|
||||
"button-add-peers": "Додати декілька пірів",
|
||||
"button-show-peer": "Показати пір",
|
||||
"button-edit-peer": "Редагувати пір",
|
||||
"peer-disabled": "Пір вимкнено, причина:",
|
||||
"peer-expiring": "Пір припиняє дію о",
|
||||
"peer-connected": "Підключено",
|
||||
"peer-not-connected": "Не підключено",
|
||||
"peer-handshake": "Останнє рукостискання:"
|
||||
},
|
||||
"users": {
|
||||
"headline": "Адміністрування користувачів",
|
||||
"table-heading": {
|
||||
"id": "ID",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Ім'я",
|
||||
"lastname": "Прізвище",
|
||||
"source": "Джерело",
|
||||
"peers": "Піри",
|
||||
"admin": "Адміністратор"
|
||||
},
|
||||
"no-user": {
|
||||
"headline": "Немає доступних користувачів",
|
||||
"abstract": "Наразі немає зареєстрованих користувачів у WireGuard Portal."
|
||||
},
|
||||
"button-add-user": "Додати користувача",
|
||||
"button-show-user": "Показати користувача",
|
||||
"button-edit-user": "Редагувати користувача",
|
||||
"user-disabled": "Користувача вимкнено, причина:",
|
||||
"user-locked": "Обліковий запис заблоковано, причина:",
|
||||
"admin": "Користувач має адміністративні привілеї",
|
||||
"no-admin": "Користувач не має адміністративних привілеїв"
|
||||
},
|
||||
|
||||
"profile": {
|
||||
"headline": "Мої VPN-піри",
|
||||
"table-heading": {
|
||||
"name": "Ім'я",
|
||||
"ip": "IP-адреси",
|
||||
"stats": "Статус",
|
||||
"interface": "Серверний інтерфейс"
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "Немає доступних пірів",
|
||||
"abstract": "Наразі немає пірів, пов'язаних із вашим профілем користувача."
|
||||
},
|
||||
"peer-connected": "Підключено",
|
||||
"button-add-peer": "Додати пір",
|
||||
"button-show-peer": "Показати пір",
|
||||
"button-edit-peer": "Редагувати пір"
|
||||
},
|
||||
"settings": {
|
||||
"headline": "Налаштування",
|
||||
"abstract": "Тут ви можете змінити особисті налаштування.",
|
||||
"api": {
|
||||
"headline": "Налаштування API",
|
||||
"abstract": "Тут ви можете налаштувати RESTful API.",
|
||||
"active-description": "API наразі активний для вашого облікового запису. Усі API-запити автентифікуються за допомогою Basic Auth. Використовуйте такі облікові дані для автентифікації.",
|
||||
"inactive-description": "API наразі неактивний. Натисніть кнопку нижче, щоб активувати його.",
|
||||
"user-label": "Ім'я користувача API:",
|
||||
"user-placeholder": "Користувач API",
|
||||
"token-label": "Пароль API:",
|
||||
"token-placeholder": "Токен API",
|
||||
"token-created-label": "Доступ до API надано:",
|
||||
"button-disable-title": "Вимкнути API, це зробить поточний токен недійсним.",
|
||||
"button-disable-text": "Вимкнути API",
|
||||
"button-enable-title": "Увімкнути API, це згенерує новий токен.",
|
||||
"button-enable-text": "Увімкнути API",
|
||||
"api-link": "Документація API"
|
||||
}
|
||||
},
|
||||
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "Обліковий запис користувача:",
|
||||
"tab-user": "Інформація",
|
||||
"tab-peers": "Піри",
|
||||
"headline-info": "Інформація про користувача:",
|
||||
"headline-notes": "Примітки:",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Ім'я",
|
||||
"lastname": "Прізвище",
|
||||
"phone": "Номер телефону",
|
||||
"department": "Відділ",
|
||||
"api-enabled": "Доступ до API",
|
||||
"disabled": "Обліковий запис вимкнено",
|
||||
"locked": "Обліковий запис заблоковано",
|
||||
"no-peers": "У користувача немає пов'язаних пірів.",
|
||||
"peers": {
|
||||
"name": "Ім'я",
|
||||
"interface": "Інтерфейс",
|
||||
"ip": "IP-адреси"
|
||||
}
|
||||
},
|
||||
"user-edit": {
|
||||
"headline-edit": "Редагування користувача:",
|
||||
"headline-new": "Новий користувач",
|
||||
"header-general": "Загальні",
|
||||
"header-personal": "Інформація про користувача",
|
||||
"header-notes": "Примітки",
|
||||
"header-state": "Стан",
|
||||
"identifier": {
|
||||
"label": "Ідентифікатор",
|
||||
"placeholder": "Унікальний ідентифікатор користувача"
|
||||
},
|
||||
"source": {
|
||||
"label": "Джерело",
|
||||
"placeholder": "Джерело користувача"
|
||||
},
|
||||
"password": {
|
||||
"label": "Пароль",
|
||||
"placeholder": "Суперсекретний пароль",
|
||||
"description": "Залиште це поле порожнім, щоб зберегти поточний пароль."
|
||||
},
|
||||
"email": {
|
||||
"label": "Електронна адреса",
|
||||
"placeholder": "Електронна адреса"
|
||||
},
|
||||
"phone": {
|
||||
"label": "Телефон",
|
||||
"placeholder": "Номер телефону"
|
||||
},
|
||||
"department": {
|
||||
"label": "Відділ",
|
||||
"placeholder": "Відділ"
|
||||
},
|
||||
"firstname": {
|
||||
"label": "Ім'я",
|
||||
"placeholder": "Ім'я"
|
||||
},
|
||||
"lastname": {
|
||||
"label": "Прізвище",
|
||||
"placeholder": "Прізвище"
|
||||
},
|
||||
"notes": {
|
||||
"label": "Примітки",
|
||||
"placeholder": ""
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Вимкнено (неможливо підключитися до WireGuard і увійти в систему)"
|
||||
},
|
||||
"locked": {
|
||||
"label": "Заблоковано (неможливо увійти, але підключення WireGuard працює)"
|
||||
},
|
||||
"admin": {
|
||||
"label": "Адміністратор"
|
||||
}
|
||||
},
|
||||
"interface-view": {
|
||||
"headline": "Конфігурація для інтерфейсу:"
|
||||
},
|
||||
"interface-edit": {
|
||||
"headline-edit": "Редагувати інтерфейс:",
|
||||
"headline-new": "Новий інтерфейс",
|
||||
"tab-interface": "Інтерфейс",
|
||||
"tab-peerdef": "За замовчуванням для пірів",
|
||||
"header-general": "Загальне",
|
||||
"header-network": "Мережа",
|
||||
"header-crypto": "Криптографія",
|
||||
"header-hooks": "Хуки інтерфейсу",
|
||||
"header-peer-hooks": "Хуки",
|
||||
"header-state": "Стан",
|
||||
"identifier": {
|
||||
"label": "Ідентифікатор",
|
||||
"placeholder": "Унікальний ідентифікатор інтерфейсу"
|
||||
},
|
||||
"mode": {
|
||||
"label": "Режим інтерфейсу",
|
||||
"server": "Серверний режим",
|
||||
"client": "Клієнтський режим",
|
||||
"any": "Невідомий режим"
|
||||
},
|
||||
"display-name": {
|
||||
"label": "Відображуване ім'я",
|
||||
"placeholder": "Описове ім'я для інтерфейсу"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Приватний ключ",
|
||||
"placeholder": "Приватний ключ"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Публічний ключ",
|
||||
"placeholder": "Публічний ключ"
|
||||
},
|
||||
"ip": {
|
||||
"label": "IP-адреси",
|
||||
"placeholder": "IP-адреси (у CIDR форматі)"
|
||||
},
|
||||
"listen-port": {
|
||||
"label": "Порт прослуховування",
|
||||
"placeholder": "Порт прослуховування"
|
||||
},
|
||||
"dns": {
|
||||
"label": "DNS сервер",
|
||||
"placeholder": "DNS сервери, які слід використовувати"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "DNS пошукові домени",
|
||||
"placeholder": "DNS пошукові префікси"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "MTU інтерфейсу (0 = залишити за замовчуванням)"
|
||||
},
|
||||
"firewall-mark": {
|
||||
"label": "Маркування Firewall",
|
||||
"placeholder": "Маркування firewall, що застосовується до вихідного трафіку. (0 = автоматично)"
|
||||
},
|
||||
"routing-table": {
|
||||
"label": "Маршрутна таблиця",
|
||||
"placeholder": "ID маршрутної таблиці",
|
||||
"description": "Особливі випадки: off = не керувати маршрутами, 0 = автоматично"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pre-Up",
|
||||
"placeholder": "Одна або декілька команд bash, розділених ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "Одна або декілька команд bash, розділених ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pre-Down",
|
||||
"placeholder": "Одна або декілька команд bash, розділених ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "Одна або декілька команд bash, розділених ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Інтерфейс відключено"
|
||||
},
|
||||
"save-config": {
|
||||
"label": "Автоматично зберігати конфігурацію wg-quick"
|
||||
},
|
||||
"defaults": {
|
||||
"endpoint": {
|
||||
"label": "Адреса кінцевої точки",
|
||||
"placeholder": "Адреса кінцевої точки",
|
||||
"description": "Адреса кінцевої точки, до якої підключатимуться піри. (наприклад, wg.example.com або wg.example.com:51820)"
|
||||
},
|
||||
"networks": {
|
||||
"label": "IP мережі",
|
||||
"placeholder": "Адреси мереж",
|
||||
"description": "Піри отримають IP-адреси з цих підмереж."
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Дозволені IP-адреси",
|
||||
"placeholder": "За замовчуванням дозволені IP-адреси"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "MTU клієнта (0 = залишити за замовчуванням)"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Інтервал Keep Alive",
|
||||
"placeholder": "Постійний Keepalive (0 = за замовчуванням)"
|
||||
}
|
||||
},
|
||||
"button-apply-defaults": "Застосувати значення за замовчуванням для пірів"
|
||||
},
|
||||
"peer-view": {
|
||||
"headline-peer": "Пір:",
|
||||
"headline-endpoint": "Кінцева точка:",
|
||||
"section-info": "Інформація про пір",
|
||||
"section-status": "Поточний стан",
|
||||
"section-config": "Налаштування",
|
||||
"identifier": "Ідентифікатор",
|
||||
"ip": "IP-адреси",
|
||||
"user": "Пов'язаний користувач",
|
||||
"notes": "Примітки",
|
||||
"expiry-status": "Закінчується",
|
||||
"disabled-status": "Відключено",
|
||||
"traffic": "Трафік",
|
||||
"connection-status": "Статистика з'єднання",
|
||||
"upload": "Передано байтів (з серверу до пір)",
|
||||
"download": "Завантажено байтів (з пір до серверу)",
|
||||
"pingable": "Відповідає на ping",
|
||||
"handshake": "Останній handshake",
|
||||
"connected-since": "Підключено з",
|
||||
"endpoint": "Кінцева точка",
|
||||
"button-download": "Завантажити конфігурацію",
|
||||
"button-email": "Надіслати конфігурацію електронною поштою"
|
||||
},
|
||||
"peer-edit": {
|
||||
"headline-edit-peer": "Редагувати пір:",
|
||||
"headline-edit-endpoint": "Редагувати кінцеву точку:",
|
||||
"headline-new-peer": "Створити пір",
|
||||
"headline-new-endpoint": "Створити кінцеву точку",
|
||||
"header-general": "Загальне",
|
||||
"header-network": "Мережа",
|
||||
"header-crypto": "Криптографія",
|
||||
"header-hooks": "Хуки (виконуються на пірі)",
|
||||
"header-state": "Стан",
|
||||
"display-name": {
|
||||
"label": "Відображуване ім'я",
|
||||
"placeholder": "Описове ім'я для пір"
|
||||
},
|
||||
"linked-user": {
|
||||
"label": "Пов'язаний користувач",
|
||||
"placeholder": "Обліковий запис користувача, що володіє цим піром"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Приватний ключ",
|
||||
"placeholder": "Приватний ключ"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Публічний ключ",
|
||||
"placeholder": "Публічний ключ"
|
||||
},
|
||||
"preshared-key": {
|
||||
"label": "Попередньо спільний ключ",
|
||||
"placeholder": "Опціональний попередньо спільний ключ"
|
||||
},
|
||||
"endpoint-public-key": {
|
||||
"label": "Публічний ключ кінцевої точки",
|
||||
"placeholder": "Публічний ключ віддаленої кінцевої точки"
|
||||
},
|
||||
"endpoint": {
|
||||
"label": "Адреса кінцевої точки",
|
||||
"placeholder": "Адреса віддаленої кінцевої точки"
|
||||
},
|
||||
"ip": {
|
||||
"label": "IP-адреси",
|
||||
"placeholder": "IP-адреси (у CIDR форматі)"
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Дозволені IP-адреси",
|
||||
"placeholder": "Дозволені IP-адреси (у CIDR форматі)"
|
||||
},
|
||||
"extra-allowed-ip": {
|
||||
"label": "Додаткові дозволені IP-адреси",
|
||||
"placeholder": "Додаткові дозволені IP (на стороні сервера)",
|
||||
"description": "Ці IP будуть додані на віддаленому інтерфейсі WireGuard як дозволені IP."
|
||||
},
|
||||
"dns": {
|
||||
"label": "DNS сервер",
|
||||
"placeholder": "DNS сервери, які слід використовувати"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "DNS пошукові домени",
|
||||
"placeholder": "DNS пошукові префікси"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Інтервал збереження зв'язку",
|
||||
"placeholder": "Постійний Keepalive (0 = за замовчуванням)"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "MTU клієнта (0 = залишити за замовчуванням)"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pre-Up",
|
||||
"placeholder": "Одна або декілька команд bash, розділених ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "Одна або декілька команд bash, розділених ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pre-Down",
|
||||
"placeholder": "Одна або декілька команд bash, розділених ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "Одна або декілька команд bash, розділених ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Пір відключено"
|
||||
},
|
||||
"ignore-global": {
|
||||
"label": "Ігнорувати глобальні налаштування"
|
||||
},
|
||||
"expires-at": {
|
||||
"label": "Дата закінчення терміну дії"
|
||||
}
|
||||
},
|
||||
"peer-multi-create": {
|
||||
"headline-peer": "Створити декілька пір",
|
||||
"headline-endpoint": "Створити декілька кінцевих точок",
|
||||
"identifiers": {
|
||||
"label": "Ідентифікатори користувача",
|
||||
"placeholder": "Ідентифікатори користувача",
|
||||
"description": "Ідентифікатор користувача (ім'я користувача), для якого слід створити пір."
|
||||
},
|
||||
"prefix": {
|
||||
"headline-peer": "Пір:",
|
||||
"headline-endpoint": "Кінцева точка:",
|
||||
"label": "Префікс відображуваного імені",
|
||||
"placeholder": "Префікс",
|
||||
"description": "Префікс, що додається до відображуваного імені пірів."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,13 +9,14 @@ import i18n from "./lang";
|
||||
import Notifications from '@kyvg/vue3-notification'
|
||||
|
||||
// Bootstrap (and theme)
|
||||
//import "bootstrap/dist/css/bootstrap.min.css"
|
||||
import "bootswatch/dist/lux/bootstrap.min.css";
|
||||
import "@/assets/custom.scss";
|
||||
import "bootstrap";
|
||||
import "./assets/base.css";
|
||||
|
||||
// Fontawesome
|
||||
// Fonts
|
||||
import "@fortawesome/fontawesome-free/js/all.js"
|
||||
import "@fontsource/nunito-sans/400.css";
|
||||
import "@fontsource/nunito-sans/600.css";
|
||||
|
||||
// Flags
|
||||
import "flag-icons/css/flag-icons.min.css"
|
||||
|
@@ -47,6 +47,14 @@ const router = createRouter({
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/ProfileView.vue')
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/SettingsView.vue')
|
||||
}
|
||||
],
|
||||
linkActiveClass: "active",
|
||||
|
@@ -12,6 +12,8 @@ export const profileStore = defineStore({
|
||||
id: 'profile',
|
||||
state: () => ({
|
||||
peers: [],
|
||||
interfaces: [],
|
||||
selectedInterfaceId: "",
|
||||
stats: {},
|
||||
statsEnabled: false,
|
||||
user: {},
|
||||
@@ -71,6 +73,7 @@ export const profileStore = defineStore({
|
||||
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
||||
},
|
||||
hasStatistics: (state) => state.statsEnabled,
|
||||
CountInterfaces: (state) => state.interfaces.length,
|
||||
},
|
||||
actions: {
|
||||
afterPageSizeChange() {
|
||||
@@ -116,6 +119,39 @@ export const profileStore = defineStore({
|
||||
this.stats = statsResponse.Stats
|
||||
this.statsEnabled = statsResponse.Enabled
|
||||
},
|
||||
setInterfaces(interfaces) {
|
||||
this.interfaces = interfaces
|
||||
this.selectedInterfaceId = interfaces.length > 0 ? interfaces[0].Identifier : ""
|
||||
this.fetching = false
|
||||
},
|
||||
async enableApi() {
|
||||
this.fetching = true
|
||||
let currentUser = authStore().user.Identifier
|
||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
|
||||
.then(this.setUser)
|
||||
.catch(error => {
|
||||
this.setPeers([])
|
||||
console.log("Failed to activate API for ", currentUser, ": ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to activate API!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async disableApi() {
|
||||
this.fetching = true
|
||||
let currentUser = authStore().user.Identifier
|
||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
|
||||
.then(this.setUser)
|
||||
.catch(error => {
|
||||
this.setPeers([])
|
||||
console.log("Failed to deactivate API for ", currentUser, ": ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to deactivate API!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async LoadPeers() {
|
||||
this.fetching = true
|
||||
let currentUser = authStore().user.Identifier
|
||||
@@ -158,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!",
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@@ -3,7 +3,7 @@ import PeerViewModal from "../components/PeerViewModal.vue";
|
||||
|
||||
import { onMounted, ref } from "vue";
|
||||
import { profileStore } from "@/stores/profile";
|
||||
import PeerEditModal from "@/components/PeerEditModal.vue";
|
||||
import UserPeerEditModal from "@/components/UserPeerEditModal.vue";
|
||||
import { settingsStore } from "@/stores/settings";
|
||||
import { humanFileSize } from "@/helpers/utils";
|
||||
|
||||
@@ -27,10 +27,18 @@ function sortBy(key) {
|
||||
profile.sortOrder = sortOrder.value;
|
||||
}
|
||||
|
||||
function friendlyInterfaceName(id, name) {
|
||||
if (name) {
|
||||
return name
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await profile.LoadUser()
|
||||
await profile.LoadPeers()
|
||||
await profile.LoadStats()
|
||||
await profile.LoadInterfaces()
|
||||
await profile.calculatePages(); // Forces to show initial page number
|
||||
})
|
||||
|
||||
@@ -38,7 +46,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<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 -->
|
||||
<div class="mt-4 row">
|
||||
@@ -56,9 +64,17 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 text-lg-end">
|
||||
<a v-if="settings.Setting('SelfProvisioning')" class="btn btn-primary ms-2" href="#"
|
||||
:title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'"><i
|
||||
class="fa fa-plus me-1"></i><i class="fa fa-user"></i></a>
|
||||
<div class="form-group" v-if="settings.Setting('SelfProvisioning')">
|
||||
<div class="input-group mb-3">
|
||||
<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 class="mt-2 table-responsive">
|
||||
|
77
frontend/src/views/SettingsView.vue
Normal file
77
frontend/src/views/SettingsView.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup>
|
||||
import PeerViewModal from "../components/PeerViewModal.vue";
|
||||
|
||||
import { onMounted, ref } from "vue";
|
||||
import { profileStore } from "@/stores/profile";
|
||||
import PeerEditModal from "@/components/PeerEditModal.vue";
|
||||
import { settingsStore } from "@/stores/settings";
|
||||
import { humanFileSize } from "@/helpers/utils";
|
||||
import {RouterLink} from "vue-router";
|
||||
import {authStore} from "../stores/auth";
|
||||
|
||||
const profile = profileStore()
|
||||
const settings = settingsStore()
|
||||
const auth = authStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await profile.LoadUser()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<h1>{{ $t('settings.headline') }}</h1>
|
||||
</div>
|
||||
|
||||
<p class="lead">{{ $t('settings.abstract') }}</p>
|
||||
|
||||
<div v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')">
|
||||
<div class="bg-light p-5" v-if="profile.user.ApiToken">
|
||||
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||
<hr class="my-4">
|
||||
<p>{{ $t('settings.api.active-description') }}</p>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('settings.api.user-label') }}</label>
|
||||
<input v-model="profile.user.Identifier" class="form-control" :placeholder="$t('settings.api.user-placeholder')" type="text" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('settings.api.token-label') }}</label>
|
||||
<input v-model="profile.user.ApiToken" class="form-control" :placeholder="$t('settings.api.token-placeholder')" type="text" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<p class="form-label mt-4">{{ $t('settings.api.token-created-label') }} {{profile.user.ApiTokenCreated}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
<div class="col-6">
|
||||
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-disable-title')" @click.prevent="profile.disableApi()" :disabled="profile.isFetching">
|
||||
<i class="fa-solid fa-minus-circle"></i> {{ $t('settings.api.button-disable-text') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<a href="/api/v1/doc.html" target="_blank" :alt="$t('settings.api.api-link')">{{ $t('settings.api.api-link') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-light p-5" v-else>
|
||||
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||
<hr class="my-4">
|
||||
<p>{{ $t('settings.api.inactive-description') }}</p>
|
||||
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-enable-title')" @click.prevent="profile.enableApi()" :disabled="profile.isFetching">
|
||||
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.api.button-enable-text') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
23
go.mod
23
go.mod
@@ -10,7 +10,8 @@ require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
github.com/prometheus-community/pro-bing v0.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/prometheus-community/pro-bing v0.6.1
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.10.0
|
||||
@@ -21,7 +22,7 @@ require (
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.4
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
golang.org/x/oauth2 v0.25.0
|
||||
golang.org/x/sys v0.29.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||
@@ -37,11 +38,10 @@ require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.7 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dchest/uniuri v1.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
@@ -56,14 +56,13 @@ require (
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.23.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.24.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-test/deep v1.1.1 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
@@ -103,15 +102,15 @@ require (
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||
golang.org/x/arch v0.13.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
google.golang.org/protobuf v1.36.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.61.6 // indirect
|
||||
modernc.org/libc v1.61.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.8.1 // indirect
|
||||
modernc.org/sqlite v1.34.4 // indirect
|
||||
|
63
go.sum
63
go.sum
@@ -34,16 +34,15 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
|
||||
github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
||||
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.2 h1:jxAJuN9fOot/cyz5Q6dUuMJF5OqQ6+5GfA8FjjQ0R4o=
|
||||
github.com/bytedance/sonic/loader v0.2.2/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
|
||||
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
@@ -96,8 +95,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
@@ -226,8 +225,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus-community/pro-bing v0.5.0 h1:Fq+4BUXKIvsPtXUY8K+04ud9dkAuFozqGmRAyNUpffY=
|
||||
github.com/prometheus-community/pro-bing v0.5.0/go.mod h1:1joR9oXdMEAcAJJvhs+8vNDvTg5thfAZcRFhcUozG2g=
|
||||
github.com/prometheus-community/pro-bing v0.6.1 h1:EQukUOma9YFZRPe4DGSscxUf9LH07rpqwisNWjSZrgU=
|
||||
github.com/prometheus-community/pro-bing v0.6.1/go.mod h1:jNCOI3D7pmTCeaoF41cNS6uaxeFY/Gmc3ffwbuJVzAQ=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
@@ -302,10 +301,11 @@ golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
|
||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
@@ -330,8 +330,9 @@ golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -401,15 +402,15 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
@@ -435,28 +436,28 @@ gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
modernc.org/cc/v4 v4.24.2 h1:uektamHbSXU7egelXcyVpMaaAsrRH4/+uMKUQAQUdOw=
|
||||
modernc.org/cc/v4 v4.24.2/go.mod h1:T1lKJZhXIi2VSqGBiB4LIbKs9NsKTbUXj4IDrmGqtTI=
|
||||
modernc.org/ccgo/v4 v4.23.5 h1:6uAwu8u3pnla3l/+UVUrDDO1HIGxHTYmFH6w+X9nsyw=
|
||||
modernc.org/ccgo/v4 v4.23.5/go.mod h1:FogrWfBdzqLWm1ku6cfr4IzEFouq2fSAPf6aSAHdAJQ=
|
||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.23.10 h1:DnDZT/H6TtoJvQmVf7d8W+lVqEZpIJY/+0ENFh1LIHE=
|
||||
modernc.org/ccgo/v4 v4.23.10/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.6.0 h1:Tiw3pezQj7PfV8k4Dzyu/vhRHR2e92kOXtTFU8pbCl4=
|
||||
modernc.org/gc/v2 v2.6.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/libc v1.61.6 h1:L2jW0wxHPCyHK0YSHaGaVlY0WxjpG/TTVdg6gRJOPqw=
|
||||
modernc.org/libc v1.61.6/go.mod h1:G+DzuaCcReUYYg4nNSfigIfTDCENdj9EByglvaRx53A=
|
||||
modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=
|
||||
modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.61.7 h1:exz8rasFniviSgh3dH7QBnQHqYh9lolA5hVYfsiwkfo=
|
||||
modernc.org/libc v1.61.7/go.mod h1:xspSrXRNVSfWfcfqgvZDVe/Hw5kv4FVC6IRfoms5v/0=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.8.1 h1:HS1HRg1jEohnuONobEq2WrLEhLyw8+J42yLFTnllm2A=
|
||||
modernc.org/memory v1.8.1/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
|
||||
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
@@ -295,6 +295,30 @@ func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, err
|
||||
return interfaces, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||
*domain.InterfaceStatus,
|
||||
error,
|
||||
) {
|
||||
if id == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var stats []domain.InterfaceStatus
|
||||
|
||||
err := r.db.WithContext(ctx).Where("identifier = ?", id).Find(&stats).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(stats) == 0 {
|
||||
return nil, domain.ErrNotFound
|
||||
}
|
||||
|
||||
stat := stats[0]
|
||||
|
||||
return &stat, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) {
|
||||
var users []domain.Interface
|
||||
|
||||
@@ -698,6 +722,30 @@ func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domai
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
var users []domain.User
|
||||
|
||||
err := r.db.WithContext(ctx).Where("email = ?", email).Find(&users).Error
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, domain.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return nil, domain.ErrNotFound
|
||||
}
|
||||
|
||||
if len(users) > 1 {
|
||||
return nil, fmt.Errorf("found multiple users with email %s: %w", email, domain.ErrNotUnique)
|
||||
}
|
||||
|
||||
user := users[0]
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||
var users []domain.User
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "WireGuard Portal API - a testing API endpoint",
|
||||
"title": "WireGuard Portal API",
|
||||
"description": "WireGuard Portal API - UI Endpoints",
|
||||
"title": "WireGuard Portal SPA-UI API",
|
||||
"contact": {
|
||||
"name": "WireGuard Portal Developers",
|
||||
"url": "https://github.com/h44z/wg-portal"
|
||||
@@ -175,6 +175,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/settings": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Configuration"
|
||||
],
|
||||
"summary": "Get the frontend settings object.",
|
||||
"operationId": "config_handleSettingsGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The JavaScript contents",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/csrf": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -499,6 +519,91 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/interface/{id}/apply-peer-defaults": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Interface"
|
||||
],
|
||||
"summary": "Apply all peer defaults to the available peers.",
|
||||
"operationId": "interfaces_handleApplyPeerDefaultsPost",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The interface identifier",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "The interface data",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Interface"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content if applying peer defaults was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/interface/{id}/save-config": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Interface"
|
||||
],
|
||||
"summary": "Save the interface configuration in wg-quick format to a file.",
|
||||
"operationId": "interfaces_handleSaveConfigPost",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The interface identifier",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content if saving the configuration was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/now": {
|
||||
"get": {
|
||||
"description": "Nothing more to describe...",
|
||||
@@ -526,9 +631,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/config-mail": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Peer"
|
||||
],
|
||||
"summary": "Send peer configuration via email.",
|
||||
"operationId": "peers_handleEmailPost",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The peer mail request data",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.PeerMailRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content if mail sending was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/config-qr/{id}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"image/png",
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
@@ -536,11 +682,20 @@
|
||||
],
|
||||
"summary": "Get peer configuration as qr code.",
|
||||
"operationId": "peers_handleQrCodeGet",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The peer identifier",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
"type": "file"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
@@ -568,6 +723,15 @@
|
||||
],
|
||||
"summary": "Get peer configuration as string.",
|
||||
"operationId": "peers_handleConfigGet",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The peer identifier",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
@@ -634,6 +798,59 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/iface/{iface}/multiplenew": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Peer"
|
||||
],
|
||||
"summary": "Create multiple new peers for the given interface.",
|
||||
"operationId": "peers_handleCreateMultiplePost",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The interface identifier",
|
||||
"name": "iface",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "The peer creation request data",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.MultiPeerRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.Peer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/iface/{iface}/new": {
|
||||
"post": {
|
||||
"produces": [
|
||||
@@ -725,6 +942,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/iface/{iface}/stats": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Peer"
|
||||
],
|
||||
"summary": "Get peer stats for the given interface.",
|
||||
"operationId": "peers_handleStatsGet",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The interface identifier",
|
||||
"name": "iface",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.PeerStats"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/{id}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -1041,6 +1299,70 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/{id}/api/disable": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Disable the REST API for the given user.",
|
||||
"operationId": "users_handleApiDisablePost",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.User"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/{id}/api/enable": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Enable the REST API for the given user.",
|
||||
"operationId": "users_handleApiEnablePost",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.User"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/{id}/peers": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -1061,6 +1383,44 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/{id}/stats": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Get peer stats for the given user.",
|
||||
"operationId": "users_handleStatsGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.PeerStats"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
@@ -1072,6 +1432,53 @@
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"model.ConfigOption-array_string": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Overridable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.ConfigOption-int": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Overridable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Value": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.ConfigOption-string": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Overridable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.ConfigOption-uint32": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Overridable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Value": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.Error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1083,25 +1490,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.Int32ConfigOption": {
|
||||
"model.ExpiryDate": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Overridable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Value": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.IntConfigOption": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Overridable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Value": {
|
||||
"type": "integer"
|
||||
"time.Time": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1290,6 +1683,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.MultiPeerRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Identifiers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"Suffix": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.Peer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1304,7 +1711,7 @@
|
||||
"description": "all allowed ip subnets, comma seperated",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.StringSliceConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-array_string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1328,7 +1735,7 @@
|
||||
"description": "the dns server that should be set if the interface is up, comma separated",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.StringSliceConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-array_string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1336,7 +1743,7 @@
|
||||
"description": "the dns search option string that should be set if the interface is up, will be appended to DnsStr",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.StringSliceConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-array_string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1344,7 +1751,7 @@
|
||||
"description": "the endpoint address",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.StringConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1352,13 +1759,17 @@
|
||||
"description": "the endpoint public key",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.StringConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ExpiresAt": {
|
||||
"description": "expiry dates for peers",
|
||||
"type": "string"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.ExpiryDate"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ExtraAllowedIPs": {
|
||||
"description": "all allowed ip subnets on the server side, comma seperated",
|
||||
@@ -1371,7 +1782,7 @@
|
||||
"description": "a firewall mark",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.Int32ConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-uint32"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1392,7 +1803,7 @@
|
||||
"description": "the device MTU",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.IntConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-int"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1404,7 +1815,7 @@
|
||||
"description": "the persistent keep-alive interval",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.IntConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-int"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1412,7 +1823,7 @@
|
||||
"description": "action that is executed after the device is down",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.StringConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1420,7 +1831,7 @@
|
||||
"description": "action that is executed after the device is up",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.StringConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1428,7 +1839,7 @@
|
||||
"description": "action that is executed before the device is down",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.StringConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1436,7 +1847,7 @@
|
||||
"description": "action that is executed before the device is up",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.StringConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1458,7 +1869,7 @@
|
||||
"description": "the routing table",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.StringConfigOption"
|
||||
"$ref": "#/definitions/model.ConfigOption-string"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1468,6 +1879,66 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.PeerMailRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Identifiers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"LinkOnly": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.PeerStatData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"BytesReceived": {
|
||||
"type": "integer"
|
||||
},
|
||||
"BytesTransmitted": {
|
||||
"type": "integer"
|
||||
},
|
||||
"EndpointAddress": {
|
||||
"type": "string"
|
||||
},
|
||||
"IsConnected": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"IsPingable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"LastHandshake": {
|
||||
"type": "string"
|
||||
},
|
||||
"LastPing": {
|
||||
"type": "string"
|
||||
},
|
||||
"LastSessionStart": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.PeerStats": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Enabled": {
|
||||
"description": "peer stats tracking enabled",
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"Stats": {
|
||||
"description": "stats, map key = Peer identifier",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/model.PeerStatData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.SessionInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1491,34 +1962,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.StringConfigOption": {
|
||||
"model.Settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Overridable": {
|
||||
"ApiAdminOnly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.StringSliceConfigOption": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Overridable": {
|
||||
"MailLinkOnly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
"PersistentConfigSupported": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"SelfProvisioning": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ApiEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ApiToken": {
|
||||
"type": "string"
|
||||
},
|
||||
"ApiTokenCreated": {
|
||||
"type": "string"
|
||||
},
|
||||
"Department": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1545,6 +2017,14 @@
|
||||
"Lastname": {
|
||||
"type": "string"
|
||||
},
|
||||
"Locked": {
|
||||
"description": "if this field is set, the user is locked",
|
||||
"type": "boolean"
|
||||
},
|
||||
"LockedReason": {
|
||||
"description": "the reason why the user has been locked",
|
||||
"type": "string"
|
||||
},
|
||||
"Notes": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@@ -1,5 +1,35 @@
|
||||
basePath: /api/v0
|
||||
definitions:
|
||||
model.ConfigOption-array_string:
|
||||
properties:
|
||||
Overridable:
|
||||
type: boolean
|
||||
Value:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
model.ConfigOption-int:
|
||||
properties:
|
||||
Overridable:
|
||||
type: boolean
|
||||
Value:
|
||||
type: integer
|
||||
type: object
|
||||
model.ConfigOption-string:
|
||||
properties:
|
||||
Overridable:
|
||||
type: boolean
|
||||
Value:
|
||||
type: string
|
||||
type: object
|
||||
model.ConfigOption-uint32:
|
||||
properties:
|
||||
Overridable:
|
||||
type: boolean
|
||||
Value:
|
||||
type: integer
|
||||
type: object
|
||||
model.Error:
|
||||
properties:
|
||||
Code:
|
||||
@@ -7,19 +37,10 @@ definitions:
|
||||
Message:
|
||||
type: string
|
||||
type: object
|
||||
model.Int32ConfigOption:
|
||||
model.ExpiryDate:
|
||||
properties:
|
||||
Overridable:
|
||||
type: boolean
|
||||
Value:
|
||||
type: integer
|
||||
type: object
|
||||
model.IntConfigOption:
|
||||
properties:
|
||||
Overridable:
|
||||
type: boolean
|
||||
Value:
|
||||
type: integer
|
||||
time.Time:
|
||||
type: string
|
||||
type: object
|
||||
model.Interface:
|
||||
properties:
|
||||
@@ -160,6 +181,15 @@ definitions:
|
||||
example: /auth/google/login
|
||||
type: string
|
||||
type: object
|
||||
model.MultiPeerRequest:
|
||||
properties:
|
||||
Identifiers:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
Suffix:
|
||||
type: string
|
||||
type: object
|
||||
model.Peer:
|
||||
properties:
|
||||
Addresses:
|
||||
@@ -169,7 +199,7 @@ definitions:
|
||||
type: array
|
||||
AllowedIPs:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.StringSliceConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-array_string'
|
||||
description: all allowed ip subnets, comma seperated
|
||||
CheckAliveAddress:
|
||||
description: optional ip address or DNS name that is used for ping checks
|
||||
@@ -185,25 +215,26 @@ definitions:
|
||||
type: string
|
||||
Dns:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.StringSliceConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-array_string'
|
||||
description: the dns server that should be set if the interface is up, comma
|
||||
separated
|
||||
DnsSearch:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.StringSliceConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-array_string'
|
||||
description: the dns search option string that should be set if the interface
|
||||
is up, will be appended to DnsStr
|
||||
Endpoint:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.StringConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-string'
|
||||
description: the endpoint address
|
||||
EndpointPublicKey:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.StringConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-string'
|
||||
description: the endpoint public key
|
||||
ExpiresAt:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.ExpiryDate'
|
||||
description: expiry dates for peers
|
||||
type: string
|
||||
ExtraAllowedIPs:
|
||||
description: all allowed ip subnets on the server side, comma seperated
|
||||
items:
|
||||
@@ -211,7 +242,7 @@ definitions:
|
||||
type: array
|
||||
FirewallMark:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.Int32ConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-uint32'
|
||||
description: a firewall mark
|
||||
Identifier:
|
||||
description: peer unique identifier
|
||||
@@ -225,30 +256,30 @@ definitions:
|
||||
type: string
|
||||
Mtu:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.IntConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-int'
|
||||
description: the device MTU
|
||||
Notes:
|
||||
description: a note field for peers
|
||||
type: string
|
||||
PersistentKeepalive:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.IntConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-int'
|
||||
description: the persistent keep-alive interval
|
||||
PostDown:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.StringConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-string'
|
||||
description: action that is executed after the device is down
|
||||
PostUp:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.StringConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-string'
|
||||
description: action that is executed after the device is up
|
||||
PreDown:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.StringConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-string'
|
||||
description: action that is executed before the device is down
|
||||
PreUp:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.StringConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-string'
|
||||
description: action that is executed before the device is up
|
||||
PresharedKey:
|
||||
description: the pre-shared Key of the peer
|
||||
@@ -263,12 +294,52 @@ definitions:
|
||||
type: string
|
||||
RoutingTable:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.StringConfigOption'
|
||||
- $ref: '#/definitions/model.ConfigOption-string'
|
||||
description: the routing table
|
||||
UserIdentifier:
|
||||
description: the owner
|
||||
type: string
|
||||
type: object
|
||||
model.PeerMailRequest:
|
||||
properties:
|
||||
Identifiers:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
LinkOnly:
|
||||
type: boolean
|
||||
type: object
|
||||
model.PeerStatData:
|
||||
properties:
|
||||
BytesReceived:
|
||||
type: integer
|
||||
BytesTransmitted:
|
||||
type: integer
|
||||
EndpointAddress:
|
||||
type: string
|
||||
IsConnected:
|
||||
type: boolean
|
||||
IsPingable:
|
||||
type: boolean
|
||||
LastHandshake:
|
||||
type: string
|
||||
LastPing:
|
||||
type: string
|
||||
LastSessionStart:
|
||||
type: string
|
||||
type: object
|
||||
model.PeerStats:
|
||||
properties:
|
||||
Enabled:
|
||||
description: peer stats tracking enabled
|
||||
example: true
|
||||
type: boolean
|
||||
Stats:
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/model.PeerStatData'
|
||||
description: stats, map key = Peer identifier
|
||||
type: object
|
||||
type: object
|
||||
model.SessionInfo:
|
||||
properties:
|
||||
IsAdmin:
|
||||
@@ -284,24 +355,25 @@ definitions:
|
||||
UserLastname:
|
||||
type: string
|
||||
type: object
|
||||
model.StringConfigOption:
|
||||
model.Settings:
|
||||
properties:
|
||||
Overridable:
|
||||
ApiAdminOnly:
|
||||
type: boolean
|
||||
Value:
|
||||
type: string
|
||||
type: object
|
||||
model.StringSliceConfigOption:
|
||||
properties:
|
||||
Overridable:
|
||||
MailLinkOnly:
|
||||
type: boolean
|
||||
PersistentConfigSupported:
|
||||
type: boolean
|
||||
SelfProvisioning:
|
||||
type: boolean
|
||||
Value:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
model.User:
|
||||
properties:
|
||||
ApiEnabled:
|
||||
type: boolean
|
||||
ApiToken:
|
||||
type: string
|
||||
ApiTokenCreated:
|
||||
type: string
|
||||
Department:
|
||||
type: string
|
||||
Disabled:
|
||||
@@ -320,6 +392,12 @@ definitions:
|
||||
type: boolean
|
||||
Lastname:
|
||||
type: string
|
||||
Locked:
|
||||
description: if this field is set, the user is locked
|
||||
type: boolean
|
||||
LockedReason:
|
||||
description: the reason why the user has been locked
|
||||
type: string
|
||||
Notes:
|
||||
type: string
|
||||
Password:
|
||||
@@ -337,8 +415,8 @@ info:
|
||||
contact:
|
||||
name: WireGuard Portal Developers
|
||||
url: https://github.com/h44z/wg-portal
|
||||
description: WireGuard Portal API - a testing API endpoint
|
||||
title: WireGuard Portal API
|
||||
description: WireGuard Portal API - UI Endpoints
|
||||
title: WireGuard Portal SPA-UI API
|
||||
version: "0.0"
|
||||
paths:
|
||||
/auth/{provider}/callback:
|
||||
@@ -448,6 +526,19 @@ paths:
|
||||
summary: Get the dynamic frontend configuration javascript.
|
||||
tags:
|
||||
- Configuration
|
||||
/config/settings:
|
||||
get:
|
||||
operationId: config_handleSettingsGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The JavaScript contents
|
||||
schema:
|
||||
type: string
|
||||
summary: Get the frontend settings object.
|
||||
tags:
|
||||
- Configuration
|
||||
/csrf:
|
||||
get:
|
||||
operationId: base_handleCsrfGet
|
||||
@@ -536,6 +627,62 @@ paths:
|
||||
summary: Update the interface record.
|
||||
tags:
|
||||
- Interface
|
||||
/interface/{id}/apply-peer-defaults:
|
||||
post:
|
||||
operationId: interfaces_handleApplyPeerDefaultsPost
|
||||
parameters:
|
||||
- description: The interface identifier
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: The interface data
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.Interface'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content if applying peer defaults was successful
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Apply all peer defaults to the available peers.
|
||||
tags:
|
||||
- Interface
|
||||
/interface/{id}/save-config:
|
||||
post:
|
||||
operationId: interfaces_handleSaveConfigPost
|
||||
parameters:
|
||||
- description: The interface identifier
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content if saving the configuration was successful
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Save the interface configuration in wg-quick format to a file.
|
||||
tags:
|
||||
- Interface
|
||||
/interface/all:
|
||||
get:
|
||||
operationId: interfaces_handleAllGet
|
||||
@@ -762,16 +909,49 @@ paths:
|
||||
summary: Update the given peer record.
|
||||
tags:
|
||||
- Peer
|
||||
/peer/config-mail:
|
||||
post:
|
||||
operationId: peers_handleEmailPost
|
||||
parameters:
|
||||
- description: The peer mail request data
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.PeerMailRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content if mail sending was successful
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Send peer configuration via email.
|
||||
tags:
|
||||
- Peer
|
||||
/peer/config-qr/{id}:
|
||||
get:
|
||||
operationId: peers_handleQrCodeGet
|
||||
parameters:
|
||||
- description: The peer identifier
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- image/png
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
type: file
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
@@ -786,6 +966,12 @@ paths:
|
||||
/peer/config/{id}:
|
||||
get:
|
||||
operationId: peers_handleConfigGet
|
||||
parameters:
|
||||
- description: The peer identifier
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -833,6 +1019,41 @@ paths:
|
||||
summary: Get peers for the given interface.
|
||||
tags:
|
||||
- Peer
|
||||
/peer/iface/{iface}/multiplenew:
|
||||
post:
|
||||
operationId: peers_handleCreateMultiplePost
|
||||
parameters:
|
||||
- description: The interface identifier
|
||||
in: path
|
||||
name: iface
|
||||
required: true
|
||||
type: string
|
||||
- description: The peer creation request data
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.MultiPeerRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.Peer'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Create multiple new peers for the given interface.
|
||||
tags:
|
||||
- Peer
|
||||
/peer/iface/{iface}/new:
|
||||
post:
|
||||
operationId: peers_handleCreatePost
|
||||
@@ -893,6 +1114,33 @@ paths:
|
||||
summary: Prepare a new peer for the given interface.
|
||||
tags:
|
||||
- Peer
|
||||
/peer/iface/{iface}/stats:
|
||||
get:
|
||||
operationId: peers_handleStatsGet
|
||||
parameters:
|
||||
- description: The interface identifier
|
||||
in: path
|
||||
name: iface
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/model.PeerStats'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Get peer stats for the given interface.
|
||||
tags:
|
||||
- Peer
|
||||
/user/{id}:
|
||||
delete:
|
||||
operationId: users_handleDelete
|
||||
@@ -972,6 +1220,48 @@ paths:
|
||||
summary: Update the user record.
|
||||
tags:
|
||||
- Users
|
||||
/user/{id}/api/disable:
|
||||
post:
|
||||
operationId: users_handleApiDisablePost
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/model.User'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Disable the REST API for the given user.
|
||||
tags:
|
||||
- Users
|
||||
/user/{id}/api/enable:
|
||||
post:
|
||||
operationId: users_handleApiEnablePost
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/model.User'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Enable the REST API for the given user.
|
||||
tags:
|
||||
- Users
|
||||
/user/{id}/peers:
|
||||
get:
|
||||
operationId: users_handlePeersGet
|
||||
@@ -984,6 +1274,10 @@ paths:
|
||||
items:
|
||||
$ref: '#/definitions/model.Peer'
|
||||
type: array
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
@@ -991,6 +1285,27 @@ paths:
|
||||
summary: Get peers for the given user.
|
||||
tags:
|
||||
- Users
|
||||
/user/{id}/stats:
|
||||
get:
|
||||
operationId: users_handleStatsGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/model.PeerStats'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Get peer stats for the given user.
|
||||
tags:
|
||||
- Users
|
||||
/user/all:
|
||||
get:
|
||||
operationId: users_handleAllGet
|
||||
|
2203
internal/app/api/core/assets/doc/v1_swagger.json
Normal file
2203
internal/app/api/core/assets/doc/v1_swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
1546
internal/app/api/core/assets/doc/v1_swagger.yaml
Normal file
1546
internal/app/api/core/assets/doc/v1_swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
3917
internal/app/api/core/assets/js/rapidoc-min.js
vendored
3917
internal/app/api/core/assets/js/rapidoc-min.js
vendored
File diff suppressed because one or more lines are too long
@@ -8,10 +8,16 @@
|
||||
<rapi-doc
|
||||
spec-url="{{ $.ApiSpecUrl }}"
|
||||
theme="dark"
|
||||
render-style="focused"
|
||||
allow-server-selection="false"
|
||||
allow-authentication="false"
|
||||
allow-authentication="true"
|
||||
load-fonts="false"
|
||||
schema-style="table"
|
||||
schema-expand-level="1"
|
||||
default-schema-tab="model"
|
||||
fill-request-fields-with-example="true"
|
||||
show-method-in-nav-bar="as-colored-block"
|
||||
show-components="true"
|
||||
allow-spec-url-load="false"
|
||||
allow-spec-file-load="false"
|
||||
allow-spec-file-download="true"
|
||||
|
@@ -88,6 +88,8 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server,
|
||||
s.server.StaticFS("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
||||
|
||||
// Setup routes
|
||||
s.server.UseRawPath = true
|
||||
s.server.UnescapePathValues = true
|
||||
s.setupRoutes(endpoints...)
|
||||
s.setupFrontendRoutes()
|
||||
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/memstore"
|
||||
@@ -10,8 +13,6 @@ import (
|
||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
csrf "github.com/utrack/gin-csrf"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type handler interface {
|
||||
@@ -20,12 +21,12 @@ type handler interface {
|
||||
}
|
||||
|
||||
// To compile the API documentation use the
|
||||
// build_tool
|
||||
// command that can be found in the $PROJECT_ROOT/internal/ports/api/build_tool directory.
|
||||
// api_build_tool
|
||||
// command that can be found in the $PROJECT_ROOT/cmd/api_build_tool directory.
|
||||
|
||||
// @title WireGuard Portal API
|
||||
// @title WireGuard Portal SPA-UI API
|
||||
// @version 0.0
|
||||
// @description WireGuard Portal API - a testing API endpoint
|
||||
// @description WireGuard Portal API - UI Endpoints
|
||||
|
||||
// @contact.name WireGuard Portal Developers
|
||||
// @contact.url https://github.com/h44z/wg-portal
|
||||
|
@@ -4,13 +4,15 @@ import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||
)
|
||||
|
||||
//go:embed frontend_config.js.gotpl
|
||||
@@ -63,12 +65,13 @@ func (e configEndpoint) handleConfigJsGet() gin.HandlerFunc {
|
||||
if err == nil {
|
||||
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
||||
}
|
||||
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host, port) // override if request comes from frontend started with npm run dev
|
||||
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host,
|
||||
port) // override if request comes from frontend started with npm run dev
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
err := e.tpl.ExecuteTemplate(buf, "frontend_config.js.gotpl", gin.H{
|
||||
"BackendUrl": backendUrl,
|
||||
"Version": "unknown",
|
||||
"Version": internal.Version,
|
||||
"SiteTitle": e.app.Config.Web.SiteTitle,
|
||||
"SiteCompanyName": e.app.Config.Web.SiteCompanyName,
|
||||
})
|
||||
@@ -96,6 +99,7 @@ func (e configEndpoint) handleSettingsGet() gin.HandlerFunc {
|
||||
MailLinkOnly: e.app.Config.Mail.LinkOnly,
|
||||
PersistentConfigSupported: e.app.Config.Advanced.ConfigStoragePath != "",
|
||||
SelfProvisioning: e.app.Config.Core.SelfProvisioningAllowed,
|
||||
ApiAdminOnly: e.app.Config.Advanced.ApiAdminOnly,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type peerEndpoint struct {
|
||||
@@ -23,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/stats", e.authenticator.LoggedIn(ScopeAdmin), e.handleStatsGet())
|
||||
apiGroup.GET("/iface/:iface/prepare", e.authenticator.LoggedIn(ScopeAdmin), e.handlePrepareGet())
|
||||
apiGroup.POST("/iface/:iface/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||
apiGroup.GET("/iface/:iface/prepare", e.authenticator.LoggedIn(), e.handlePrepareGet())
|
||||
apiGroup.POST("/iface/:iface/new", e.authenticator.LoggedIn(), e.handleCreatePost())
|
||||
apiGroup.POST("/iface/:iface/multiplenew", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreateMultiplePost())
|
||||
apiGroup.GET("/config-qr/:id", e.handleQrCodeGet())
|
||||
apiGroup.POST("/config-mail", e.handleEmailPost())
|
||||
@@ -57,7 +58,8 @@ func (e peerEndpoint) handleAllGet() gin.HandlerFunc {
|
||||
|
||||
_, peers, err := e.app.GetInterfaceAndPeers(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,7 +90,8 @@ func (e peerEndpoint) handleSingleGet() gin.HandlerFunc {
|
||||
|
||||
peer, err := e.app.GetPeer(ctx, domain.PeerIdentifier(peerId))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,7 +122,8 @@ func (e peerEndpoint) handlePrepareGet() gin.HandlerFunc {
|
||||
|
||||
peer, err := e.app.PreparePeer(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -163,7 +167,8 @@ func (e peerEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
|
||||
newPeer, err := e.app.CreatePeer(ctx, model.NewDomainPeer(&p))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -200,9 +205,11 @@ func (e peerEndpoint) handleCreateMultiplePost() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
newPeers, err := e.app.CreateMultiplePeers(ctx, domain.InterfaceIdentifier(interfaceId), model.NewDomainPeerCreationRequest(&req))
|
||||
newPeers, err := e.app.CreateMultiplePeers(ctx, domain.InterfaceIdentifier(interfaceId),
|
||||
model.NewDomainPeerCreationRequest(&req))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -246,7 +253,8 @@ func (e peerEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||
|
||||
updatedPeer, err := e.app.UpdatePeer(ctx, model.NewDomainPeer(&p))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -277,7 +285,8 @@ func (e peerEndpoint) handleDelete() gin.HandlerFunc {
|
||||
|
||||
err := e.app.DeletePeer(ctx, domain.PeerIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -333,9 +342,10 @@ func (e peerEndpoint) handleConfigGet() gin.HandlerFunc {
|
||||
// @ID peers_handleQrCodeGet
|
||||
// @Tags Peer
|
||||
// @Summary Get peer configuration as qr code.
|
||||
// @Produce png
|
||||
// @Produce json
|
||||
// @Param id path string true "The peer identifier"
|
||||
// @Success 200 {object} string
|
||||
// @Success 200 {file} binary
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /peer/config-qr/{id} [get]
|
||||
@@ -403,7 +413,8 @@ func (e peerEndpoint) handleEmailPost() gin.HandlerFunc {
|
||||
}
|
||||
err = e.app.SendPeerEmail(ctx, req.LinkOnly, peerIds...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -434,7 +445,8 @@ func (e peerEndpoint) handleStatsGet() gin.HandlerFunc {
|
||||
|
||||
stats, err := e.app.GetPeerStats(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -1,11 +1,12 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type userEndpoint struct {
|
||||
@@ -27,6 +28,9 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
|
||||
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
|
||||
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/disable", e.authenticator.UserIdMatch("id"), e.handleApiDisablePost())
|
||||
}
|
||||
|
||||
// handleAllGet returns a gorm handler function.
|
||||
@@ -44,7 +48,8 @@ func (e userEndpoint) handleAllGet() gin.HandlerFunc {
|
||||
|
||||
users, err := e.app.GetAllUsers(ctx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,11 +79,12 @@ func (e userEndpoint) handleSingleGet() gin.HandlerFunc {
|
||||
|
||||
user, err := e.app.GetUser(ctx, domain.UserIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.NewUser(user))
|
||||
c.JSON(http.StatusOK, model.NewUser(user, true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,11 +124,12 @@ func (e userEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||
|
||||
updateUser, err := e.app.UpdateUser(ctx, model.NewDomainUser(&user))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.NewUser(updateUser))
|
||||
c.JSON(http.StatusOK, model.NewUser(updateUser, false))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,11 +157,12 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
|
||||
newUser, err := e.app.CreateUser(ctx, model.NewDomainUser(&user))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, model.NewUser(newUser))
|
||||
c.JSON(http.StatusOK, model.NewUser(newUser, false))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +171,7 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
// @ID users_handlePeersGet
|
||||
// @Tags Users
|
||||
// @Summary Get peers for the given user.
|
||||
// @Param id path string true "The user identifier"
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.Peer
|
||||
// @Failure 400 {object} model.Error
|
||||
@@ -172,15 +181,17 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
interfaceId := Base64UrlDecode(c.Param("id"))
|
||||
if interfaceId == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||
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.GetUserPeers(ctx, domain.UserIdentifier(interfaceId))
|
||||
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(userId))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -193,6 +204,7 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
|
||||
// @ID users_handleStatsGet
|
||||
// @Tags Users
|
||||
// @Summary Get peer stats for the given user.
|
||||
// @Param id path string true "The user identifier"
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.PeerStats
|
||||
// @Failure 400 {object} model.Error
|
||||
@@ -204,13 +216,15 @@ func (e userEndpoint) handleStatsGet() gin.HandlerFunc {
|
||||
|
||||
userId := Base64UrlDecode(c.Param("id"))
|
||||
if userId == "" {
|
||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||
c.JSON(http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := e.app.GetUserPeerStats(ctx, domain.UserIdentifier(userId))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -218,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.
|
||||
//
|
||||
// @ID users_handleDelete
|
||||
@@ -241,10 +288,75 @@ func (e userEndpoint) handleDelete() gin.HandlerFunc {
|
||||
|
||||
err := e.app.DeleteUser(ctx, domain.UserIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
c.JSON(http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// handleApiEnablePost returns a gorm handler function.
|
||||
//
|
||||
// @ID users_handleApiEnablePost
|
||||
// @Tags Users
|
||||
// @Summary Enable the REST API for the given user.
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.User
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /user/{id}/api/enable [post]
|
||||
func (e userEndpoint) handleApiEnablePost() 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
|
||||
}
|
||||
|
||||
user, err := e.app.ActivateApi(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.NewUser(user, true))
|
||||
}
|
||||
}
|
||||
|
||||
// handleApiDisablePost returns a gorm handler function.
|
||||
//
|
||||
// @ID users_handleApiDisablePost
|
||||
// @Tags Users
|
||||
// @Summary Disable the REST API for the given user.
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.User
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /user/{id}/api/disable [post]
|
||||
func (e userEndpoint) handleApiDisablePost() 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
|
||||
}
|
||||
|
||||
user, err := e.app.DeactivateApi(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.NewUser(user, false))
|
||||
}
|
||||
}
|
||||
|
@@ -9,4 +9,5 @@ type Settings struct {
|
||||
MailLinkOnly bool `json:"MailLinkOnly"`
|
||||
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
||||
SelfProvisioning bool `json:"SelfProvisioning"`
|
||||
ApiAdminOnly bool `json:"ApiAdminOnly"`
|
||||
}
|
||||
|
@@ -109,7 +109,11 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
|
||||
func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface {
|
||||
results := make([]Interface, len(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
|
||||
|
@@ -25,37 +25,50 @@ type User struct {
|
||||
Locked bool `json:"Locked"` // if this field is set, the user is locked
|
||||
LockedReason string `json:"LockedReason"` // the reason why the user has been locked
|
||||
|
||||
ApiToken string `json:"ApiToken"`
|
||||
ApiTokenCreated *time.Time `json:"ApiTokenCreated,omitempty"`
|
||||
ApiEnabled bool `json:"ApiEnabled"`
|
||||
|
||||
// Calculated
|
||||
|
||||
PeerCount int `json:"PeerCount"`
|
||||
}
|
||||
|
||||
func NewUser(src *domain.User) *User {
|
||||
return &User{
|
||||
Identifier: string(src.Identifier),
|
||||
Email: src.Email,
|
||||
Source: string(src.Source),
|
||||
ProviderName: src.ProviderName,
|
||||
IsAdmin: src.IsAdmin,
|
||||
Firstname: src.Firstname,
|
||||
Lastname: src.Lastname,
|
||||
Phone: src.Phone,
|
||||
Department: src.Department,
|
||||
Notes: src.Notes,
|
||||
Password: "", // never fill password
|
||||
Disabled: src.IsDisabled(),
|
||||
DisabledReason: src.DisabledReason,
|
||||
Locked: src.IsLocked(),
|
||||
LockedReason: src.LockedReason,
|
||||
func NewUser(src *domain.User, exposeCreds bool) *User {
|
||||
u := &User{
|
||||
Identifier: string(src.Identifier),
|
||||
Email: src.Email,
|
||||
Source: string(src.Source),
|
||||
ProviderName: src.ProviderName,
|
||||
IsAdmin: src.IsAdmin,
|
||||
Firstname: src.Firstname,
|
||||
Lastname: src.Lastname,
|
||||
Phone: src.Phone,
|
||||
Department: src.Department,
|
||||
Notes: src.Notes,
|
||||
Password: "", // never fill password
|
||||
Disabled: src.IsDisabled(),
|
||||
DisabledReason: src.DisabledReason,
|
||||
Locked: src.IsLocked(),
|
||||
LockedReason: src.LockedReason,
|
||||
ApiToken: "", // by default, do not expose API token
|
||||
ApiTokenCreated: src.ApiTokenCreated,
|
||||
ApiEnabled: src.IsApiEnabled(),
|
||||
|
||||
PeerCount: src.LinkedPeerCount,
|
||||
}
|
||||
|
||||
if exposeCreds {
|
||||
u.ApiToken = src.ApiToken
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func NewUsers(src []domain.User) []User {
|
||||
results := make([]User, len(src))
|
||||
for i := range src {
|
||||
results[i] = *NewUser(&src[i])
|
||||
results[i] = *NewUser(&src[i], false)
|
||||
}
|
||||
|
||||
return results
|
||||
|
109
internal/app/api/v1/backend/interface_service.go
Normal file
109
internal/app/api/v1/backend/interface_service.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type InterfaceServiceInterfaceManagerRepo interface {
|
||||
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
|
||||
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
|
||||
}
|
||||
|
||||
type InterfaceService struct {
|
||||
cfg *config.Config
|
||||
|
||||
interfaces InterfaceServiceInterfaceManagerRepo
|
||||
users PeerServiceUserManagerRepo
|
||||
}
|
||||
|
||||
func NewInterfaceService(cfg *config.Config, interfaces InterfaceServiceInterfaceManagerRepo) *InterfaceService {
|
||||
return &InterfaceService{
|
||||
cfg: cfg,
|
||||
interfaces: interfaces,
|
||||
}
|
||||
}
|
||||
|
||||
func (s InterfaceService) GetAll(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
interfaces, interfacePeers, err := s.interfaces.GetAllInterfacesAndPeers(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return interfaces, interfacePeers, nil
|
||||
}
|
||||
|
||||
func (s InterfaceService) GetById(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||
*domain.Interface,
|
||||
[]domain.Peer,
|
||||
error,
|
||||
) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
interfaceData, interfacePeers, err := s.interfaces.GetInterfaceAndPeers(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return interfaceData, interfacePeers, nil
|
||||
}
|
||||
|
||||
func (s InterfaceService) Create(ctx context.Context, iface *domain.Interface) (*domain.Interface, error) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createdInterface, err := s.interfaces.CreateInterface(ctx, iface)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createdInterface, nil
|
||||
}
|
||||
|
||||
func (s InterfaceService) Update(ctx context.Context, id domain.InterfaceIdentifier, iface *domain.Interface) (
|
||||
*domain.Interface,
|
||||
[]domain.Peer,
|
||||
error,
|
||||
) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if iface.Identifier != id {
|
||||
return nil, nil, fmt.Errorf("interface id mismatch: %s != %s: %w",
|
||||
iface.Identifier, id, domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
updatedInterface, updatedPeers, err := s.interfaces.UpdateInterface(ctx, iface)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return updatedInterface, updatedPeers, nil
|
||||
}
|
||||
|
||||
func (s InterfaceService) Delete(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := s.interfaces.DeleteInterface(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
131
internal/app/api/v1/backend/metrics_service.go
Normal file
131
internal/app/api/v1/backend/metrics_service.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type MetricsServiceDatabaseRepo interface {
|
||||
GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error)
|
||||
GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||
*domain.InterfaceStatus,
|
||||
error,
|
||||
)
|
||||
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||
}
|
||||
|
||||
type MetricsServiceUserManagerRepo interface {
|
||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
}
|
||||
|
||||
type MetricsServicePeerManagerRepo interface {
|
||||
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||
}
|
||||
|
||||
type MetricsService struct {
|
||||
cfg *config.Config
|
||||
|
||||
db MetricsServiceDatabaseRepo
|
||||
users MetricsServiceUserManagerRepo
|
||||
peers MetricsServicePeerManagerRepo
|
||||
}
|
||||
|
||||
func NewMetricsService(
|
||||
cfg *config.Config,
|
||||
db MetricsServiceDatabaseRepo,
|
||||
users MetricsServiceUserManagerRepo,
|
||||
peers MetricsServicePeerManagerRepo,
|
||||
) *MetricsService {
|
||||
return &MetricsService{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
users: users,
|
||||
peers: peers,
|
||||
}
|
||||
}
|
||||
|
||||
func (m MetricsService) GetForInterface(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||
*domain.InterfaceStatus,
|
||||
error,
|
||||
) {
|
||||
if !m.cfg.Statistics.CollectInterfaceData {
|
||||
return nil, fmt.Errorf("interface statistics collection is disabled")
|
||||
}
|
||||
|
||||
// validate admin rights
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
interfaceStats, err := m.db.GetInterfaceStats(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch stats for interface %s: %w", id, err)
|
||||
}
|
||||
|
||||
return interfaceStats, nil
|
||||
}
|
||||
|
||||
func (m MetricsService) GetForUser(ctx context.Context, id domain.UserIdentifier) (
|
||||
*domain.User,
|
||||
[]domain.PeerStatus,
|
||||
error,
|
||||
) {
|
||||
if !m.cfg.Statistics.CollectPeerData {
|
||||
return nil, nil, fmt.Errorf("statistics collection is disabled")
|
||||
}
|
||||
|
||||
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
user, err := m.users.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
peers, err := m.db.GetUserPeers(ctx, user.Identifier)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to fetch peers for user %s: %w", user.Identifier, err)
|
||||
}
|
||||
|
||||
peerIds := make([]domain.PeerIdentifier, len(peers))
|
||||
for i, peer := range peers {
|
||||
peerIds[i] = peer.Identifier
|
||||
}
|
||||
|
||||
peerStats, err := m.db.GetPeersStats(ctx, peerIds...)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to fetch peer stats for user %s: %w", user.Identifier, err)
|
||||
}
|
||||
|
||||
return user, peerStats, nil
|
||||
}
|
||||
|
||||
func (m MetricsService) GetForPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.PeerStatus, error) {
|
||||
if !m.cfg.Statistics.CollectPeerData {
|
||||
return nil, fmt.Errorf("peer statistics collection is disabled")
|
||||
}
|
||||
|
||||
peer, err := m.peers.GetPeer(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peerStats, err := m.db.GetPeersStats(ctx, peer.Identifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch stats for peer %s: %w", peer.Identifier, err)
|
||||
}
|
||||
|
||||
if len(peerStats) == 0 {
|
||||
return nil, fmt.Errorf("no stats found for peer %s: %w", peer.Identifier, domain.ErrNotFound)
|
||||
}
|
||||
|
||||
return &peerStats[0], nil
|
||||
}
|
143
internal/app/api/v1/backend/peer_service.go
Normal file
143
internal/app/api/v1/backend/peer_service.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type PeerServicePeerManagerRepo interface {
|
||||
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||
CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
|
||||
UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
|
||||
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||
}
|
||||
|
||||
type PeerServiceUserManagerRepo interface {
|
||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
}
|
||||
|
||||
type PeerService struct {
|
||||
cfg *config.Config
|
||||
|
||||
peers PeerServicePeerManagerRepo
|
||||
users PeerServiceUserManagerRepo
|
||||
}
|
||||
|
||||
func NewPeerService(
|
||||
cfg *config.Config,
|
||||
peers PeerServicePeerManagerRepo,
|
||||
users PeerServiceUserManagerRepo,
|
||||
) *PeerService {
|
||||
return &PeerService{
|
||||
cfg: cfg,
|
||||
peers: peers,
|
||||
users: users,
|
||||
}
|
||||
}
|
||||
|
||||
func (s PeerService) GetForInterface(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, interfacePeers, err := s.peers.GetInterfaceAndPeers(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return interfacePeers, nil
|
||||
}
|
||||
|
||||
func (s PeerService) GetForUser(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
||||
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||
}
|
||||
|
||||
user, err := s.users.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userPeers, err := s.peers.GetUserPeers(ctx, user.Identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userPeers, nil
|
||||
}
|
||||
|
||||
func (s PeerService) GetById(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
|
||||
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||
}
|
||||
|
||||
peer, err := s.peers.GetPeer(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the user has access rights to the requested peer.
|
||||
// If the peer is not linked to any user, access is granted only for admins.
|
||||
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
func (s PeerService) Create(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if peer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
|
||||
return nil, fmt.Errorf("peer id mismatch: %s != %s: %w",
|
||||
peer.Identifier, peer.Interface.PublicKey, domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
createdPeer, err := s.peers.CreatePeer(ctx, peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createdPeer, nil
|
||||
}
|
||||
|
||||
func (s PeerService) Update(ctx context.Context, _ domain.PeerIdentifier, peer *domain.Peer) (
|
||||
*domain.Peer,
|
||||
error,
|
||||
) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedPeer, err := s.peers.UpdatePeer(ctx, peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updatedPeer, nil
|
||||
}
|
||||
|
||||
func (s PeerService) Delete(ctx context.Context, id domain.PeerIdentifier) error {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := s.peers.DeletePeer(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
174
internal/app/api/v1/backend/provisioning_service.go
Normal file
174
internal/app/api/v1/backend/provisioning_service.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type ProvisioningServiceUserManagerRepo interface {
|
||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||
}
|
||||
|
||||
type ProvisioningServicePeerManagerRepo interface {
|
||||
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||
GetUserPeers(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
|
||||
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
|
||||
CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||
}
|
||||
|
||||
type ProvisioningServiceConfigFileManagerRepo interface {
|
||||
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
||||
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
||||
}
|
||||
|
||||
type ProvisioningService struct {
|
||||
cfg *config.Config
|
||||
|
||||
users ProvisioningServiceUserManagerRepo
|
||||
peers ProvisioningServicePeerManagerRepo
|
||||
configFiles ProvisioningServiceConfigFileManagerRepo
|
||||
}
|
||||
|
||||
func NewProvisioningService(
|
||||
cfg *config.Config,
|
||||
users ProvisioningServiceUserManagerRepo,
|
||||
peers ProvisioningServicePeerManagerRepo,
|
||||
configFiles ProvisioningServiceConfigFileManagerRepo,
|
||||
) *ProvisioningService {
|
||||
return &ProvisioningService{
|
||||
cfg: cfg,
|
||||
|
||||
users: users,
|
||||
peers: peers,
|
||||
configFiles: configFiles,
|
||||
}
|
||||
}
|
||||
|
||||
func (p ProvisioningService) GetUserAndPeers(
|
||||
ctx context.Context,
|
||||
userId domain.UserIdentifier,
|
||||
email string,
|
||||
) (*domain.User, []domain.Peer, error) {
|
||||
// first fetch user
|
||||
var user *domain.User
|
||||
switch {
|
||||
case userId != "":
|
||||
u, err := p.users.GetUser(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
user = u
|
||||
case email != "":
|
||||
u, err := p.users.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
user = u
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("either UserId or Email must be set: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if err := domain.ValidateUserAccessRights(ctx, user.Identifier); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
peers, err := p.peers.GetUserPeers(ctx, user.Identifier)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return user, peers, nil
|
||||
}
|
||||
|
||||
func (p ProvisioningService) GetPeerConfig(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error) {
|
||||
peer, err := p.peers.GetPeer(ctx, peerId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peerCfgData, err := io.ReadAll(peerCfgReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return peerCfgData, nil
|
||||
}
|
||||
|
||||
func (p ProvisioningService) GetPeerQrPng(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error) {
|
||||
peer, err := p.peers.GetPeer(ctx, peerId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peerCfgQrData, err := io.ReadAll(peerCfgQrReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return peerCfgQrData, nil
|
||||
}
|
||||
|
||||
func (p ProvisioningService) NewPeer(ctx context.Context, req models.ProvisioningRequest) (*domain.Peer, error) {
|
||||
if req.UserIdentifier == "" {
|
||||
req.UserIdentifier = string(domain.GetUserInfo(ctx).Id) // use authenticated user id if not set
|
||||
}
|
||||
|
||||
// check permissions
|
||||
if err := domain.ValidateUserAccessRights(ctx, domain.UserIdentifier(req.UserIdentifier)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !p.cfg.Core.SelfProvisioningAllowed {
|
||||
// only admins can create new peers if self-provisioning is disabled
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// prepare new peer
|
||||
peer, err := p.peers.PreparePeer(ctx, domain.InterfaceIdentifier(req.InterfaceIdentifier))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare new peer: %w", err)
|
||||
}
|
||||
peer.UserIdentifier = domain.UserIdentifier(req.UserIdentifier) // overwrite context user id with the one from the request
|
||||
if req.PublicKey != "" {
|
||||
peer.Identifier = domain.PeerIdentifier(req.PublicKey)
|
||||
peer.Interface.PublicKey = req.PublicKey
|
||||
peer.Interface.PrivateKey = "" // clear private key if public key is set, WireGuard Portal does not know the private key in that case
|
||||
}
|
||||
if req.PresharedKey != "" {
|
||||
peer.PresharedKey = domain.PreSharedKey(req.PresharedKey)
|
||||
}
|
||||
peer.GenerateDisplayName("API")
|
||||
|
||||
// save new peer
|
||||
peer, err = p.peers.CreatePeer(ctx, peer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new peer: %w", err)
|
||||
}
|
||||
|
||||
return peer, nil
|
||||
}
|
107
internal/app/api/v1/backend/user_service.go
Normal file
107
internal/app/api/v1/backend/user_service.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type UserManagerRepo interface {
|
||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||
}
|
||||
|
||||
type UserService struct {
|
||||
cfg *config.Config
|
||||
|
||||
users UserManagerRepo
|
||||
}
|
||||
|
||||
func NewUserService(cfg *config.Config, users UserManagerRepo) *UserService {
|
||||
return &UserService{
|
||||
cfg: cfg,
|
||||
users: users,
|
||||
}
|
||||
}
|
||||
|
||||
func (s UserService) GetAll(ctx context.Context) ([]domain.User, error) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allUsers, err := s.users.GetAllUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return allUsers, nil
|
||||
}
|
||||
|
||||
func (s UserService) GetById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||
}
|
||||
|
||||
user, err := s.users.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s UserService) Create(ctx context.Context, user *domain.User) (*domain.User, error) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createdUser, err := s.users.CreateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createdUser, nil
|
||||
}
|
||||
|
||||
func (s UserService) Update(ctx context.Context, id domain.UserIdentifier, user *domain.User) (
|
||||
*domain.User,
|
||||
error,
|
||||
) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if id != user.Identifier {
|
||||
return nil, fmt.Errorf("user id mismatch: %s != %s: %w", id, user.Identifier, domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
updatedUser, err := s.users.UpdateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return updatedUser, nil
|
||||
}
|
||||
|
||||
func (s UserService) Delete(ctx context.Context, id domain.UserIdentifier) error {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := s.users.DeleteUser(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
81
internal/app/api/v1/handlers/base.go
Normal file
81
internal/app/api/v1/handlers/base.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/core"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
GetName() string
|
||||
RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler)
|
||||
}
|
||||
|
||||
// To compile the API documentation use the
|
||||
// api_build_tool
|
||||
// command that can be found in the $PROJECT_ROOT/cmd/api_build_tool directory.
|
||||
|
||||
// @title WireGuard Portal Public API
|
||||
// @version 1.0
|
||||
// @description The WireGuard Portal REST API enables efficient management of WireGuard VPN configurations through a set of JSON-based endpoints.
|
||||
// @description It supports creating and editing peers, interfaces, and user profiles, while also providing role-based access control and auditing.
|
||||
// @description This API allows seamless integration with external tools or scripts for automated network configuration and administration.
|
||||
|
||||
// @license.name MIT
|
||||
// @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt
|
||||
|
||||
// @contact.name WireGuard Portal Project
|
||||
// @contact.url https://github.com/h44z/wg-portal
|
||||
|
||||
// @securityDefinitions.basic BasicAuth
|
||||
|
||||
// @BasePath /api/v1
|
||||
// @query.collection.format multi
|
||||
|
||||
func NewRestApi(userSource UserSource, handlers ...Handler) core.ApiEndpointSetupFunc {
|
||||
authenticator := &authenticationHandler{
|
||||
userSource: userSource,
|
||||
}
|
||||
|
||||
return func() (core.ApiVersion, core.GroupSetupFn) {
|
||||
return "v1", func(group *gin.RouterGroup) {
|
||||
group.Use(cors.Default())
|
||||
|
||||
// Handler functions
|
||||
for _, h := range handlers {
|
||||
h.RegisterRoutes(group, authenticator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ParseServiceError(err error) (int, models.Error) {
|
||||
if err == nil {
|
||||
return 500, models.Error{
|
||||
Code: 500,
|
||||
Message: "unknown server error",
|
||||
}
|
||||
}
|
||||
|
||||
code := http.StatusInternalServerError
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrNotFound):
|
||||
code = http.StatusNotFound
|
||||
case errors.Is(err, domain.ErrNoPermission):
|
||||
code = http.StatusForbidden
|
||||
case errors.Is(err, domain.ErrDuplicateEntry):
|
||||
code = http.StatusConflict
|
||||
case errors.Is(err, domain.ErrInvalidData):
|
||||
code = http.StatusBadRequest
|
||||
}
|
||||
|
||||
return code, models.Error{
|
||||
Code: code,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
220
internal/app/api/v1/handlers/endpoint_interface.go
Normal file
220
internal/app/api/v1/handlers/endpoint_interface.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type InterfaceEndpointInterfaceService interface {
|
||||
GetAll(context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||
GetById(context.Context, domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||
Create(context.Context, *domain.Interface) (*domain.Interface, error)
|
||||
Update(context.Context, domain.InterfaceIdentifier, *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||
Delete(context.Context, domain.InterfaceIdentifier) error
|
||||
}
|
||||
|
||||
type InterfaceEndpoint struct {
|
||||
interfaces InterfaceEndpointInterfaceService
|
||||
}
|
||||
|
||||
func NewInterfaceEndpoint(interfaceService InterfaceEndpointInterfaceService) *InterfaceEndpoint {
|
||||
return &InterfaceEndpoint{
|
||||
interfaces: interfaceService,
|
||||
}
|
||||
}
|
||||
|
||||
func (e InterfaceEndpoint) GetName() string {
|
||||
return "InterfaceEndpoint"
|
||||
}
|
||||
|
||||
func (e InterfaceEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||
apiGroup := g.Group("/interface", authenticator.LoggedIn())
|
||||
|
||||
apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
||||
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleByIdGet())
|
||||
|
||||
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||
}
|
||||
|
||||
// handleAllGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID interface_handleAllGet
|
||||
// @Tags Interfaces
|
||||
// @Summary Get all interface records.
|
||||
// @Produce json
|
||||
// @Success 200 {object} []models.Interface
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /interface/all [get]
|
||||
// @Security BasicAuth
|
||||
func (e InterfaceEndpoint) handleAllGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
allInterfaces, allPeersPerInterface, err := e.interfaces.GetAll(ctx)
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewInterfaces(allInterfaces, allPeersPerInterface))
|
||||
}
|
||||
}
|
||||
|
||||
// handleByIdGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID interfaces_handleByIdGet
|
||||
// @Tags Interfaces
|
||||
// @Summary Get a specific interface record by its identifier.
|
||||
// @Param id path string true "The interface identifier."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Interface
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /interface/by-id/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e InterfaceEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||
return
|
||||
}
|
||||
|
||||
iface, interfacePeers, err := e.interfaces.GetById(ctx, domain.InterfaceIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewInterface(iface, interfacePeers))
|
||||
}
|
||||
}
|
||||
|
||||
// handleCreatePost returns a gorm handler function.
|
||||
//
|
||||
// @ID interfaces_handleCreatePost
|
||||
// @Tags Interfaces
|
||||
// @Summary Create a new interface record.
|
||||
// @Param request body models.Interface true "The interface data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Interface
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 409 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /interface/new [post]
|
||||
// @Security BasicAuth
|
||||
func (e InterfaceEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
var iface models.Interface
|
||||
err := c.BindJSON(&iface)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
newInterface, err := e.interfaces.Create(ctx, models.NewDomainInterface(&iface))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewInterface(newInterface, nil))
|
||||
}
|
||||
}
|
||||
|
||||
// handleUpdatePut returns a gorm handler function.
|
||||
//
|
||||
// @ID interfaces_handleUpdatePut
|
||||
// @Tags Interfaces
|
||||
// @Summary Update an interface record.
|
||||
// @Param id path string true "The interface identifier."
|
||||
// @Param request body models.Interface true "The interface data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Interface
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /interface/by-id/{id} [put]
|
||||
// @Security BasicAuth
|
||||
func (e InterfaceEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||
return
|
||||
}
|
||||
|
||||
var iface models.Interface
|
||||
err := c.BindJSON(&iface)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updatedInterface, updatedInterfacePeers, err := e.interfaces.Update(
|
||||
ctx,
|
||||
domain.InterfaceIdentifier(id),
|
||||
models.NewDomainInterface(&iface),
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewInterface(updatedInterface, updatedInterfacePeers))
|
||||
}
|
||||
}
|
||||
|
||||
// handleDelete returns a gorm handler function.
|
||||
//
|
||||
// @ID interfaces_handleDelete
|
||||
// @Tags Interfaces
|
||||
// @Summary Delete the interface record.
|
||||
// @Param id path string true "The interface identifier."
|
||||
// @Produce json
|
||||
// @Success 204 "No content if deletion was successful."
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /interface/by-id/{id} [delete]
|
||||
// @Security BasicAuth
|
||||
func (e InterfaceEndpoint) handleDelete() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||
return
|
||||
}
|
||||
|
||||
err := e.interfaces.Delete(ctx, domain.InterfaceIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
140
internal/app/api/v1/handlers/endpoint_metrics.go
Normal file
140
internal/app/api/v1/handlers/endpoint_metrics.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type MetricsEndpointStatisticsService interface {
|
||||
GetForInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.InterfaceStatus, error)
|
||||
GetForUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, []domain.PeerStatus, error)
|
||||
GetForPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.PeerStatus, error)
|
||||
}
|
||||
|
||||
type MetricsEndpoint struct {
|
||||
metrics MetricsEndpointStatisticsService
|
||||
}
|
||||
|
||||
func NewMetricsEndpoint(metrics MetricsEndpointStatisticsService) *MetricsEndpoint {
|
||||
return &MetricsEndpoint{
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
func (e MetricsEndpoint) GetName() string {
|
||||
return "MetricsEndpoint"
|
||||
}
|
||||
|
||||
func (e MetricsEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||
apiGroup := g.Group("/metrics", authenticator.LoggedIn())
|
||||
|
||||
apiGroup.GET("/by-interface/:id", authenticator.LoggedIn(ScopeAdmin), e.handleMetricsForInterfaceGet())
|
||||
apiGroup.GET("/by-user/:id", authenticator.LoggedIn(), e.handleMetricsForUserGet())
|
||||
apiGroup.GET("/by-peer/:id", authenticator.LoggedIn(), e.handleMetricsForPeerGet())
|
||||
}
|
||||
|
||||
// handleMetricsForInterfaceGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID metrics_handleMetricsForInterfaceGet
|
||||
// @Tags Metrics
|
||||
// @Summary Get all metrics for a WireGuard Portal interface.
|
||||
// @Param id path string true "The WireGuard interface identifier."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.InterfaceMetrics
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /metrics/by-interface/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e MetricsEndpoint) handleMetricsForInterfaceGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||
return
|
||||
}
|
||||
|
||||
interfaceMetrics, err := e.metrics.GetForInterface(ctx, domain.InterfaceIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewInterfaceMetrics(interfaceMetrics))
|
||||
}
|
||||
}
|
||||
|
||||
// handleMetricsForUserGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID metrics_handleMetricsForUserGet
|
||||
// @Tags Metrics
|
||||
// @Summary Get all metrics for a WireGuard Portal user.
|
||||
// @Param id path string true "The user identifier."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.UserMetrics
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /metrics/by-user/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e MetricsEndpoint) handleMetricsForUserGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||
return
|
||||
}
|
||||
|
||||
user, userMetrics, err := e.metrics.GetForUser(ctx, domain.UserIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewUserMetrics(user, userMetrics))
|
||||
}
|
||||
}
|
||||
|
||||
// handleMetricsForPeerGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID metrics_handleMetricsForPeerGet
|
||||
// @Tags Metrics
|
||||
// @Summary Get all metrics for a WireGuard Portal peer.
|
||||
// @Param id path string true "The peer identifier (public key)."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.PeerMetrics
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /metrics/by-peer/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e MetricsEndpoint) handleMetricsForPeerGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||
return
|
||||
}
|
||||
|
||||
peerMetrics, err := e.metrics.GetForPeer(ctx, domain.PeerIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeerMetrics(peerMetrics))
|
||||
}
|
||||
}
|
261
internal/app/api/v1/handlers/endpoint_peer.go
Normal file
261
internal/app/api/v1/handlers/endpoint_peer.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type PeerService interface {
|
||||
GetForInterface(context.Context, domain.InterfaceIdentifier) ([]domain.Peer, error)
|
||||
GetForUser(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
|
||||
GetById(context.Context, domain.PeerIdentifier) (*domain.Peer, error)
|
||||
Create(context.Context, *domain.Peer) (*domain.Peer, error)
|
||||
Update(context.Context, domain.PeerIdentifier, *domain.Peer) (*domain.Peer, error)
|
||||
Delete(context.Context, domain.PeerIdentifier) error
|
||||
}
|
||||
|
||||
type PeerEndpoint struct {
|
||||
peers PeerService
|
||||
}
|
||||
|
||||
func NewPeerEndpoint(peerService PeerService) *PeerEndpoint {
|
||||
return &PeerEndpoint{
|
||||
peers: peerService,
|
||||
}
|
||||
}
|
||||
|
||||
func (e PeerEndpoint) GetName() string {
|
||||
return "PeerEndpoint"
|
||||
}
|
||||
|
||||
func (e PeerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||
apiGroup := g.Group("/peer", authenticator.LoggedIn())
|
||||
|
||||
apiGroup.GET("/by-interface/:id", authenticator.LoggedIn(ScopeAdmin), e.handleAllForInterfaceGet())
|
||||
apiGroup.GET("/by-user/:id", authenticator.LoggedIn(), e.handleAllForUserGet())
|
||||
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
|
||||
|
||||
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||
}
|
||||
|
||||
// handleAllForInterfaceGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID peers_handleAllForInterfaceGet
|
||||
// @Tags Peers
|
||||
// @Summary Get all peer records for a given WireGuard interface.
|
||||
// @Param id path string true "The WireGuard interface identifier."
|
||||
// @Produce json
|
||||
// @Success 200 {object} []models.Peer
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/by-interface/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleAllForInterfaceGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||
return
|
||||
}
|
||||
|
||||
interfacePeers, err := e.peers.GetForInterface(ctx, domain.InterfaceIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
|
||||
}
|
||||
}
|
||||
|
||||
// handleAllForUserGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID peers_handleAllForUserGet
|
||||
// @Tags Peers
|
||||
// @Summary Get all peer records for a given user.
|
||||
// @Description Normal users can only access their own records. Admins can access all records.
|
||||
// @Param id path string true "The user identifier."
|
||||
// @Produce json
|
||||
// @Success 200 {object} []models.Peer
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/by-user/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleAllForUserGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
return
|
||||
}
|
||||
|
||||
interfacePeers, err := e.peers.GetForUser(ctx, domain.UserIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
|
||||
}
|
||||
}
|
||||
|
||||
// handleByIdGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID peers_handleByIdGet
|
||||
// @Tags Peers
|
||||
// @Summary Get a specific peer record by its identifier (public key).
|
||||
// @Description Normal users can only access their own records. Admins can access all records.
|
||||
// @Param id path string true "The peer identifier (public key)."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Peer
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/by-id/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||
return
|
||||
}
|
||||
|
||||
peer, err := e.peers.GetById(ctx, domain.PeerIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeer(peer))
|
||||
}
|
||||
}
|
||||
|
||||
// handleCreatePost returns a gorm handler function.
|
||||
//
|
||||
// @ID peers_handleCreatePost
|
||||
// @Tags Peers
|
||||
// @Summary Create a new peer record.
|
||||
// @Description Only admins can create new records.
|
||||
// @Param request body models.Peer true "The peer data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Peer
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 409 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/new [post]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
var peer models.Peer
|
||||
err := c.BindJSON(&peer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
newPeer, err := e.peers.Create(ctx, models.NewDomainPeer(&peer))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeer(newPeer))
|
||||
}
|
||||
}
|
||||
|
||||
// handleUpdatePut returns a gorm handler function.
|
||||
//
|
||||
// @ID peers_handleUpdatePut
|
||||
// @Tags Peers
|
||||
// @Summary Update a peer record.
|
||||
// @Description Only admins can update existing records.
|
||||
// @Param id path string true "The peer identifier."
|
||||
// @Param request body models.Peer true "The peer data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Peer
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/by-id/{id} [put]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||
return
|
||||
}
|
||||
|
||||
var peer models.Peer
|
||||
err := c.BindJSON(&peer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updatedPeer, err := e.peers.Update(ctx, domain.PeerIdentifier(id), models.NewDomainPeer(&peer))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeer(updatedPeer))
|
||||
}
|
||||
}
|
||||
|
||||
// handleDelete returns a gorm handler function.
|
||||
//
|
||||
// @ID peers_handleDelete
|
||||
// @Tags Peers
|
||||
// @Summary Delete the peer record.
|
||||
// @Param id path string true "The peer identifier."
|
||||
// @Produce json
|
||||
// @Success 204 "No content if deletion was successful."
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/by-id/{id} [delete]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleDelete() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||
return
|
||||
}
|
||||
|
||||
err := e.peers.Delete(ctx, domain.PeerIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
195
internal/app/api/v1/handlers/endpoint_provisioning.go
Normal file
195
internal/app/api/v1/handlers/endpoint_provisioning.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type ProvisioningEndpointProvisioningService interface {
|
||||
GetUserAndPeers(ctx context.Context, userId domain.UserIdentifier, email string) (
|
||||
*domain.User,
|
||||
[]domain.Peer,
|
||||
error,
|
||||
)
|
||||
GetPeerConfig(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
|
||||
GetPeerQrPng(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
|
||||
NewPeer(ctx context.Context, req models.ProvisioningRequest) (*domain.Peer, error)
|
||||
}
|
||||
|
||||
type ProvisioningEndpoint struct {
|
||||
provisioning ProvisioningEndpointProvisioningService
|
||||
}
|
||||
|
||||
func NewProvisioningEndpoint(provisioning ProvisioningEndpointProvisioningService) *ProvisioningEndpoint {
|
||||
return &ProvisioningEndpoint{
|
||||
provisioning: provisioning,
|
||||
}
|
||||
}
|
||||
|
||||
func (e ProvisioningEndpoint) GetName() string {
|
||||
return "ProvisioningEndpoint"
|
||||
}
|
||||
|
||||
func (e ProvisioningEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||
apiGroup := g.Group("/provisioning", authenticator.LoggedIn())
|
||||
|
||||
apiGroup.GET("/data/user-info", authenticator.LoggedIn(), e.handleUserInfoGet())
|
||||
apiGroup.GET("/data/peer-config", authenticator.LoggedIn(), e.handlePeerConfigGet())
|
||||
apiGroup.GET("/data/peer-qr", authenticator.LoggedIn(), e.handlePeerQrGet())
|
||||
|
||||
apiGroup.POST("/new-peer", authenticator.LoggedIn(), e.handleNewPeerPost())
|
||||
}
|
||||
|
||||
// handleUserInfoGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID provisioning_handleUserInfoGet
|
||||
// @Tags Provisioning
|
||||
// @Summary Get information about all peer records for a given user.
|
||||
// @Description Normal users can only access their own record. Admins can access all records.
|
||||
// @Param UserId query string false "The user identifier that should be queried. If not set, the authenticated user is used."
|
||||
// @Param Email query string false "The email address that should be queried. If UserId is set, this is ignored."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.UserInformation
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /provisioning/data/user-info [get]
|
||||
// @Security BasicAuth
|
||||
func (e ProvisioningEndpoint) handleUserInfoGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := strings.TrimSpace(c.Query("UserId"))
|
||||
email := strings.TrimSpace(c.Query("Email"))
|
||||
|
||||
if id == "" && email == "" {
|
||||
id = string(domain.GetUserInfo(ctx).Id)
|
||||
}
|
||||
|
||||
user, peers, err := e.provisioning.GetUserAndPeers(ctx, domain.UserIdentifier(id), email)
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewUserInformation(user, peers))
|
||||
}
|
||||
}
|
||||
|
||||
// handlePeerConfigGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID provisioning_handlePeerConfigGet
|
||||
// @Tags Provisioning
|
||||
// @Summary Get the peer configuration in wg-quick format.
|
||||
// @Description Normal users can only access their own record. Admins can access all records.
|
||||
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
|
||||
// @Produce plain
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "The WireGuard configuration file"
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /provisioning/data/peer-config [get]
|
||||
// @Security BasicAuth
|
||||
func (e ProvisioningEndpoint) handlePeerConfigGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := strings.TrimSpace(c.Query("PeerId"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||
return
|
||||
}
|
||||
|
||||
peerConfig, err := e.provisioning.GetPeerConfig(ctx, domain.PeerIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, "text/plain", peerConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// handlePeerQrGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID provisioning_handlePeerQrGet
|
||||
// @Tags Provisioning
|
||||
// @Summary Get the peer configuration as QR code.
|
||||
// @Description Normal users can only access their own record. Admins can access all records.
|
||||
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
|
||||
// @Produce png
|
||||
// @Produce json
|
||||
// @Success 200 {file} binary "The WireGuard configuration QR code"
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /provisioning/data/peer-qr [get]
|
||||
// @Security BasicAuth
|
||||
func (e ProvisioningEndpoint) handlePeerQrGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := strings.TrimSpace(c.Query("PeerId"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||
return
|
||||
}
|
||||
|
||||
peerConfigQrCode, err := e.provisioning.GetPeerQrPng(ctx, domain.PeerIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, "image/png", peerConfigQrCode)
|
||||
}
|
||||
}
|
||||
|
||||
// handleNewPeerPost returns a gorm Handler function.
|
||||
//
|
||||
// @ID provisioning_handleNewPeerPost
|
||||
// @Tags Provisioning
|
||||
// @Summary Create a new peer for the given interface and user.
|
||||
// @Description Normal users can only create new peers if self provisioning is allowed. Admins can always add new peers.
|
||||
// @Param request body models.ProvisioningRequest true "Provisioning request model."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Peer
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /provisioning/new-peer [post]
|
||||
// @Security BasicAuth
|
||||
func (e ProvisioningEndpoint) handleNewPeerPost() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
var req models.ProvisioningRequest
|
||||
err := c.BindJSON(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
peer, err := e.provisioning.NewPeer(ctx, req)
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeer(peer))
|
||||
}
|
||||
}
|
218
internal/app/api/v1/handlers/endpoint_user.go
Normal file
218
internal/app/api/v1/handlers/endpoint_user.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type UserService interface {
|
||||
GetAll(ctx context.Context) ([]domain.User, error)
|
||||
GetById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
Create(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
Update(ctx context.Context, id domain.UserIdentifier, user *domain.User) (*domain.User, error)
|
||||
Delete(ctx context.Context, id domain.UserIdentifier) error
|
||||
}
|
||||
|
||||
type UserEndpoint struct {
|
||||
users UserService
|
||||
}
|
||||
|
||||
func NewUserEndpoint(userService UserService) *UserEndpoint {
|
||||
return &UserEndpoint{
|
||||
users: userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (e UserEndpoint) GetName() string {
|
||||
return "UserEndpoint"
|
||||
}
|
||||
|
||||
func (e UserEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||
apiGroup := g.Group("/user", authenticator.LoggedIn())
|
||||
|
||||
apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
||||
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
|
||||
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||
}
|
||||
|
||||
// handleAllGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID users_handleAllGet
|
||||
// @Tags Users
|
||||
// @Summary Get all user records.
|
||||
// @Produce json
|
||||
// @Success 200 {object} []models.User
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /user/all [get]
|
||||
// @Security BasicAuth
|
||||
func (e UserEndpoint) handleAllGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
users, err := e.users.GetAll(ctx)
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewUsers(users))
|
||||
}
|
||||
}
|
||||
|
||||
// handleByIdGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID users_handleByIdGet
|
||||
// @Tags Users
|
||||
// @Summary Get a specific user record by its internal identifier.
|
||||
// @Description Normal users can only access their own record. Admins can access all records.
|
||||
// @Param id path string true "The user identifier."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /user/by-id/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e UserEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := e.users.GetById(ctx, domain.UserIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewUser(user, true))
|
||||
}
|
||||
}
|
||||
|
||||
// handleCreatePost returns a gorm handler function.
|
||||
//
|
||||
// @ID users_handleCreatePost
|
||||
// @Tags Users
|
||||
// @Summary Create a new user record.
|
||||
// @Description Only admins can create new records.
|
||||
// @Param request body models.User true "The user data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 409 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /user/new [post]
|
||||
// @Security BasicAuth
|
||||
func (e UserEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
var user models.User
|
||||
err := c.BindJSON(&user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
newUser, err := e.users.Create(ctx, models.NewDomainUser(&user))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewUser(newUser, true))
|
||||
}
|
||||
}
|
||||
|
||||
// handleUpdatePut returns a gorm handler function.
|
||||
//
|
||||
// @ID users_handleUpdatePut
|
||||
// @Tags Users
|
||||
// @Summary Update a user record.
|
||||
// @Description Only admins can update existing records.
|
||||
// @Param id path string true "The user identifier."
|
||||
// @Param request body models.User true "The user data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /user/by-id/{id} [put]
|
||||
// @Security BasicAuth
|
||||
func (e UserEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err := c.BindJSON(&user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updateUser, err := e.users.Update(ctx, domain.UserIdentifier(id), models.NewDomainUser(&user))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewUser(updateUser, true))
|
||||
}
|
||||
}
|
||||
|
||||
// handleDelete returns a gorm handler function.
|
||||
//
|
||||
// @ID users_handleDelete
|
||||
// @Tags Users
|
||||
// @Summary Delete the user record.
|
||||
// @Param id path string true "The user identifier."
|
||||
// @Produce json
|
||||
// @Success 204 "No content if deletion was successful."
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /user/by-id/{id} [delete]
|
||||
// @Security BasicAuth
|
||||
func (e UserEndpoint) handleDelete() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
return
|
||||
}
|
||||
|
||||
err := e.users.Delete(ctx, domain.UserIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
92
internal/app/api/v1/handlers/middleware_authentication.go
Normal file
92
internal/app/api/v1/handlers/middleware_authentication.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type Scope string
|
||||
|
||||
const (
|
||||
ScopeAdmin Scope = "ADMIN" // Admin scope contains all other scopes
|
||||
)
|
||||
|
||||
type UserSource interface {
|
||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
}
|
||||
|
||||
type authenticationHandler struct {
|
||||
userSource UserSource
|
||||
}
|
||||
|
||||
// LoggedIn checks if a user is logged in. If scopes are given, they are validated as well.
|
||||
func (h authenticationHandler) LoggedIn(scopes ...Scope) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
username, password, ok := c.Request.BasicAuth()
|
||||
if !ok || username == "" || password == "" {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "missing credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// check if user exists in DB
|
||||
|
||||
ctx := domain.SetUserInfo(c.Request.Context(), domain.SystemAdminContextUserInfo())
|
||||
user, err := h.userSource.GetUser(ctx, domain.UserIdentifier(username))
|
||||
if err != nil {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// validate API token
|
||||
if err := user.CheckApiToken(password); err != nil {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
if !UserHasScopes(user, scopes...) {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusForbidden, model.Error{Code: http.StatusForbidden, Message: "not enough permissions"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(domain.CtxUserInfo, &domain.ContextUserInfo{
|
||||
Id: user.Identifier,
|
||||
IsAdmin: user.IsAdmin,
|
||||
})
|
||||
|
||||
// Continue down the chain to Handler etc
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func UserHasScopes(user *domain.User, scopes ...Scope) bool {
|
||||
// No scopes give, so the check should succeed
|
||||
if len(scopes) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// check if user has admin scope
|
||||
if user.IsAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if admin scope is required
|
||||
for _, scope := range scopes {
|
||||
if scope == ScopeAdmin {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
46
internal/app/api/v1/models/model_options.go
Normal file
46
internal/app/api/v1/models/model_options.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type ConfigOption[T any] struct {
|
||||
Value T `json:"Value"`
|
||||
Overridable bool `json:"Overridable,omitempty"`
|
||||
}
|
||||
|
||||
func NewConfigOption[T any](value T, overridable bool) ConfigOption[T] {
|
||||
return ConfigOption[T]{
|
||||
Value: value,
|
||||
Overridable: overridable,
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigOptionFromDomain[T any](opt domain.ConfigOption[T]) ConfigOption[T] {
|
||||
return ConfigOption[T]{
|
||||
Value: opt.Value,
|
||||
Overridable: opt.Overridable,
|
||||
}
|
||||
}
|
||||
|
||||
func ConfigOptionToDomain[T any](opt ConfigOption[T]) domain.ConfigOption[T] {
|
||||
return domain.ConfigOption[T]{
|
||||
Value: opt.Value,
|
||||
Overridable: opt.Overridable,
|
||||
}
|
||||
}
|
||||
|
||||
func StringSliceConfigOptionFromDomain(opt domain.ConfigOption[string]) ConfigOption[[]string] {
|
||||
return ConfigOption[[]string]{
|
||||
Value: internal.SliceString(opt.Value),
|
||||
Overridable: opt.Overridable,
|
||||
}
|
||||
}
|
||||
|
||||
func StringSliceConfigOptionToDomain(opt ConfigOption[[]string]) domain.ConfigOption[string] {
|
||||
return domain.ConfigOption[string]{
|
||||
Value: internal.SliceToString(opt.Value),
|
||||
Overridable: opt.Overridable,
|
||||
}
|
||||
}
|
8
internal/app/api/v1/models/models.go
Normal file
8
internal/app/api/v1/models/models.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package models
|
||||
|
||||
// Error represents an error response.
|
||||
type Error struct {
|
||||
Code int `json:"Code"` // HTTP status code.
|
||||
Message string `json:"Message"` // Error message.
|
||||
Details string `json:"Details,omitempty"` // Additional error details.
|
||||
}
|
201
internal/app/api/v1/models/models_interface.go
Normal file
201
internal/app/api/v1/models/models_interface.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
// Interface represents a WireGuard interface.
|
||||
type Interface struct {
|
||||
// Identifier is the unique identifier of the interface. It is always equal to the device name of the interface.
|
||||
Identifier string `json:"Identifier" example:"wg0" binding:"required"`
|
||||
// DisplayName is a nice display name / description for the interface.
|
||||
DisplayName string `json:"DisplayName" binding:"omitempty,max=64" example:"My Interface"`
|
||||
// Mode is the interface type, either 'server', 'client' or 'any'. The mode specifies how WireGuard Portal handles peers for this interface.
|
||||
Mode string `json:"Mode" example:"server" binding:"required,oneof=server client any"`
|
||||
// PrivateKey is the private key of the interface.
|
||||
PrivateKey string `json:"PrivateKey" example:"gI6EdUSYvn8ugXOt8QQD6Yc+JyiZxIhp3GInSWRfWGE=" binding:"required,len=44"`
|
||||
// PublicKey is the public key of the server interface. The public key is used by peers to connect to the server.
|
||||
PublicKey string `json:"PublicKey" example:"HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=" binding:"required,len=44"`
|
||||
// Disabled is a flag that specifies if the interface is enabled (up) or not (down). Disabled interfaces are not able to accept connections.
|
||||
Disabled bool `json:"Disabled" example:"false"`
|
||||
// DisabledReason is the reason why the interface has been disabled.
|
||||
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:"This is a reason why the interface has been disabled."`
|
||||
// SaveConfig is a flag that specifies if the configuration should be saved to the configuration file (wgX.conf in wg-quick format).
|
||||
SaveConfig bool `json:"SaveConfig" example:"false"`
|
||||
|
||||
// ListenPort is the listening port, for example: 51820. The listening port is only required for server interfaces.
|
||||
ListenPort int `json:"ListenPort" binding:"omitempty,min=1,max=65535" example:"51820"`
|
||||
// Addresses is a list of IP addresses (in CIDR format) that are assigned to the interface.
|
||||
Addresses []string `json:"Addresses" binding:"omitempty,dive,cidr" example:"10.11.12.1/24"`
|
||||
// Dns is a list of DNS servers that should be set if the interface is up.
|
||||
Dns []string `json:"Dns" binding:"omitempty,dive,ip" example:"1.1.1.1"`
|
||||
// DnsSearch is the dns search option string that should be set if the interface is up, will be appended to Dns servers.
|
||||
DnsSearch []string `json:"DnsSearch" binding:"omitempty,dive,fqdn" example:"wg.local"`
|
||||
// Mtu is the device MTU of the interface.
|
||||
Mtu int `json:"Mtu" binding:"omitempty,min=1,max=9000" example:"1420"`
|
||||
// FirewallMark is an optional firewall mark which is used to handle interface traffic.
|
||||
FirewallMark uint32 `json:"FirewallMark"`
|
||||
// RoutingTable is an optional routing table which is used to route interface traffic.
|
||||
RoutingTable string `json:"RoutingTable"`
|
||||
|
||||
// PreUp is an optional action that is executed before the device is up.
|
||||
PreUp string `json:"PreUp" example:"echo 'Interface is up'"`
|
||||
// PostUp is an optional action that is executed after the device is up.
|
||||
PostUp string `json:"PostUp" example:"iptables -A FORWARD -i %i -j ACCEPT"`
|
||||
// PreDown is an optional action that is executed before the device is down.
|
||||
PreDown string `json:"PreDown" example:"iptables -D FORWARD -i %i -j ACCEPT"`
|
||||
// PostDown is an optional action that is executed after the device is down.
|
||||
PostDown string `json:"PostDown" example:"echo 'Interface is down'"`
|
||||
|
||||
// PeerDefNetwork specifies the default subnets from which new peers will get their IP addresses. The subnet is specified in CIDR format.
|
||||
PeerDefNetwork []string `json:"PeerDefNetwork" example:"10.11.12.0/24"`
|
||||
// PeerDefDns specifies the default dns servers for a new peer.
|
||||
PeerDefDns []string `json:"PeerDefDns" example:"8.8.8.8"`
|
||||
// PeerDefDnsSearch specifies the default dns search options for a new peer.
|
||||
PeerDefDnsSearch []string `json:"PeerDefDnsSearch" example:"wg.local"`
|
||||
// PeerDefEndpoint specifies the default endpoint for a new peer.
|
||||
PeerDefEndpoint string `json:"PeerDefEndpoint" example:"wg.example.com:51820"`
|
||||
// PeerDefAllowedIPs specifies the default allowed IP addresses for a new peer.
|
||||
PeerDefAllowedIPs []string `json:"PeerDefAllowedIPs" example:"10.11.12.0/24"`
|
||||
// PeerDefMtu specifies the default device MTU for a new peer.
|
||||
PeerDefMtu int `json:"PeerDefMtu" example:"1420"`
|
||||
// PeerDefPersistentKeepalive specifies the default persistent keep-alive value in seconds for a new peer.
|
||||
PeerDefPersistentKeepalive int `json:"PeerDefPersistentKeepalive" example:"25"`
|
||||
// PeerDefFirewallMark specifies the default firewall mark for a new peer.
|
||||
PeerDefFirewallMark uint32 `json:"PeerDefFirewallMark"`
|
||||
// PeerDefRoutingTable specifies the default routing table for a new peer.
|
||||
PeerDefRoutingTable string `json:"PeerDefRoutingTable"`
|
||||
|
||||
// PeerDefPreUp specifies the default action that is executed before the device is up for a new peer.
|
||||
PeerDefPreUp string `json:"PeerDefPreUp"`
|
||||
// PeerDefPostUp specifies the default action that is executed after the device is up for a new peer.
|
||||
PeerDefPostUp string `json:"PeerDefPostUp"`
|
||||
// PeerDefPreDown specifies the default action that is executed before the device is down for a new peer.
|
||||
PeerDefPreDown string `json:"PeerDefPreDown"`
|
||||
// PeerDefPostDown specifies the default action that is executed after the device is down for a new peer.
|
||||
PeerDefPostDown string `json:"PeerDefPostDown"`
|
||||
|
||||
// Calculated values
|
||||
|
||||
// EnabledPeers is the number of enabled peers for this interface. Only enabled peers are able to connect.
|
||||
EnabledPeers int `json:"EnabledPeers" readonly:"true"`
|
||||
// TotalPeers is the total number of peers for this interface.
|
||||
TotalPeers int `json:"TotalPeers" readonly:"true"`
|
||||
}
|
||||
|
||||
func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
|
||||
iface := &Interface{
|
||||
Identifier: string(src.Identifier),
|
||||
DisplayName: src.DisplayName,
|
||||
Mode: string(src.Type),
|
||||
PrivateKey: src.PrivateKey,
|
||||
PublicKey: src.PublicKey,
|
||||
Disabled: src.IsDisabled(),
|
||||
DisabledReason: src.DisabledReason,
|
||||
SaveConfig: src.SaveConfig,
|
||||
ListenPort: src.ListenPort,
|
||||
Addresses: domain.CidrsToStringSlice(src.Addresses),
|
||||
Dns: internal.SliceString(src.DnsStr),
|
||||
DnsSearch: internal.SliceString(src.DnsSearchStr),
|
||||
Mtu: src.Mtu,
|
||||
FirewallMark: src.FirewallMark,
|
||||
RoutingTable: src.RoutingTable,
|
||||
PreUp: src.PreUp,
|
||||
PostUp: src.PostUp,
|
||||
PreDown: src.PreDown,
|
||||
PostDown: src.PostDown,
|
||||
PeerDefNetwork: internal.SliceString(src.PeerDefNetworkStr),
|
||||
PeerDefDns: internal.SliceString(src.PeerDefDnsStr),
|
||||
PeerDefDnsSearch: internal.SliceString(src.PeerDefDnsSearchStr),
|
||||
PeerDefEndpoint: src.PeerDefEndpoint,
|
||||
PeerDefAllowedIPs: internal.SliceString(src.PeerDefAllowedIPsStr),
|
||||
PeerDefMtu: src.PeerDefMtu,
|
||||
PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive,
|
||||
PeerDefFirewallMark: src.PeerDefFirewallMark,
|
||||
PeerDefRoutingTable: src.PeerDefRoutingTable,
|
||||
PeerDefPreUp: src.PeerDefPreUp,
|
||||
PeerDefPostUp: src.PeerDefPostUp,
|
||||
PeerDefPreDown: src.PeerDefPreDown,
|
||||
PeerDefPostDown: src.PeerDefPostDown,
|
||||
|
||||
EnabledPeers: 0,
|
||||
TotalPeers: 0,
|
||||
}
|
||||
|
||||
if len(peers) > 0 {
|
||||
iface.TotalPeers = len(peers)
|
||||
|
||||
activePeers := 0
|
||||
for _, peer := range peers {
|
||||
if !peer.IsDisabled() {
|
||||
activePeers++
|
||||
}
|
||||
}
|
||||
iface.EnabledPeers = activePeers
|
||||
}
|
||||
|
||||
return iface
|
||||
}
|
||||
|
||||
func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface {
|
||||
results := make([]Interface, len(src))
|
||||
for i := range src {
|
||||
results[i] = *NewInterface(&src[i], srcPeers[i])
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func NewDomainInterface(src *Interface) *domain.Interface {
|
||||
now := time.Now()
|
||||
|
||||
cidrs, _ := domain.CidrsFromArray(src.Addresses)
|
||||
|
||||
res := &domain.Interface{
|
||||
BaseModel: domain.BaseModel{},
|
||||
Identifier: domain.InterfaceIdentifier(src.Identifier),
|
||||
KeyPair: domain.KeyPair{
|
||||
PrivateKey: src.PrivateKey,
|
||||
PublicKey: src.PublicKey,
|
||||
},
|
||||
ListenPort: src.ListenPort,
|
||||
Addresses: cidrs,
|
||||
DnsStr: internal.SliceToString(src.Dns),
|
||||
DnsSearchStr: internal.SliceToString(src.DnsSearch),
|
||||
Mtu: src.Mtu,
|
||||
FirewallMark: src.FirewallMark,
|
||||
RoutingTable: src.RoutingTable,
|
||||
PreUp: src.PreUp,
|
||||
PostUp: src.PostUp,
|
||||
PreDown: src.PreDown,
|
||||
PostDown: src.PostDown,
|
||||
SaveConfig: src.SaveConfig,
|
||||
DisplayName: src.DisplayName,
|
||||
Type: domain.InterfaceType(src.Mode),
|
||||
DriverType: "", // currently unused
|
||||
Disabled: nil, // set below
|
||||
DisabledReason: src.DisabledReason,
|
||||
PeerDefNetworkStr: internal.SliceToString(src.PeerDefNetwork),
|
||||
PeerDefDnsStr: internal.SliceToString(src.PeerDefDns),
|
||||
PeerDefDnsSearchStr: internal.SliceToString(src.PeerDefDnsSearch),
|
||||
PeerDefEndpoint: src.PeerDefEndpoint,
|
||||
PeerDefAllowedIPsStr: internal.SliceToString(src.PeerDefAllowedIPs),
|
||||
PeerDefMtu: src.PeerDefMtu,
|
||||
PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive,
|
||||
PeerDefFirewallMark: src.PeerDefFirewallMark,
|
||||
PeerDefRoutingTable: src.PeerDefRoutingTable,
|
||||
PeerDefPreUp: src.PeerDefPreUp,
|
||||
PeerDefPostUp: src.PeerDefPostUp,
|
||||
PeerDefPreDown: src.PeerDefPreDown,
|
||||
PeerDefPostDown: src.PeerDefPostDown,
|
||||
}
|
||||
|
||||
if src.Disabled {
|
||||
res.Disabled = &now
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
105
internal/app/api/v1/models/models_metrics.go
Normal file
105
internal/app/api/v1/models/models_metrics.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
// PeerMetrics represents the metrics of a WireGuard peer.
|
||||
type PeerMetrics struct {
|
||||
// The unique identifier of the peer.
|
||||
PeerIdentifier string `json:"PeerIdentifier" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="`
|
||||
|
||||
// If this field is set, the peer is pingable.
|
||||
IsPingable bool `json:"IsPingable" example:"true"`
|
||||
// The last time the peer responded to a ICMP ping request.
|
||||
LastPing *time.Time `json:"LastPing" example:"2021-01-01T12:00:00Z"`
|
||||
|
||||
// The number of bytes received by the peer.
|
||||
BytesReceived uint64 `json:"BytesReceived" example:"123456789"`
|
||||
// The number of bytes transmitted by the peer.
|
||||
BytesTransmitted uint64 `json:"BytesTransmitted" example:"123456789"`
|
||||
|
||||
// The last time the peer initiated a handshake.
|
||||
LastHandshake *time.Time `json:"LastHandshake" example:"2021-01-01T12:00:00Z"`
|
||||
// The current endpoint address of the peer.
|
||||
Endpoint string `json:"Endpoint" example:"12.34.56.78"`
|
||||
// The last time the peer initiated a session.
|
||||
LastSessionStart *time.Time `json:"LastSessionStart" example:"2021-01-01T12:00:00Z"`
|
||||
}
|
||||
|
||||
func NewPeerMetrics(src *domain.PeerStatus) *PeerMetrics {
|
||||
return &PeerMetrics{
|
||||
PeerIdentifier: string(src.PeerId),
|
||||
IsPingable: src.IsPingable,
|
||||
LastPing: src.LastPing,
|
||||
BytesReceived: src.BytesReceived,
|
||||
BytesTransmitted: src.BytesTransmitted,
|
||||
LastHandshake: src.LastHandshake,
|
||||
Endpoint: src.Endpoint,
|
||||
LastSessionStart: src.LastSessionStart,
|
||||
}
|
||||
}
|
||||
|
||||
// InterfaceMetrics represents the metrics of a WireGuard interface.
|
||||
type InterfaceMetrics struct {
|
||||
// The unique identifier of the interface.
|
||||
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0"`
|
||||
|
||||
// The number of bytes received by the interface.
|
||||
BytesReceived uint64 `json:"BytesReceived" example:"123456789"`
|
||||
// The number of bytes transmitted by the interface.
|
||||
BytesTransmitted uint64 `json:"BytesTransmitted" example:"123456789"`
|
||||
}
|
||||
|
||||
func NewInterfaceMetrics(src *domain.InterfaceStatus) *InterfaceMetrics {
|
||||
return &InterfaceMetrics{
|
||||
InterfaceIdentifier: string(src.InterfaceId),
|
||||
BytesReceived: src.BytesReceived,
|
||||
BytesTransmitted: src.BytesTransmitted,
|
||||
}
|
||||
}
|
||||
|
||||
// UserMetrics represents the metrics of a WireGuard user.
|
||||
type UserMetrics struct {
|
||||
// The unique identifier of the user.
|
||||
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||
|
||||
// PeerCount represents the number of peers linked to the user.
|
||||
PeerCount int `json:"PeerCount" example:"2"`
|
||||
|
||||
// The total number of bytes received by the user. This is the sum of all bytes received by the peers linked to the user.
|
||||
BytesReceived uint64 `json:"BytesReceived" example:"123456789"`
|
||||
// The total number of bytes transmitted by the user. This is the sum of all bytes transmitted by the peers linked to the user.
|
||||
BytesTransmitted uint64 `json:"BytesTransmitted" example:"123456789"`
|
||||
|
||||
// PeerMetrics represents the metrics of the peers linked to the user.
|
||||
PeerMetrics []PeerMetrics `json:"PeerMetrics"`
|
||||
}
|
||||
|
||||
func NewUserMetrics(srcUser *domain.User, src []domain.PeerStatus) *UserMetrics {
|
||||
if srcUser == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
um := &UserMetrics{
|
||||
UserIdentifier: string(srcUser.Identifier),
|
||||
PeerCount: srcUser.LinkedPeerCount,
|
||||
PeerMetrics: []PeerMetrics{},
|
||||
|
||||
BytesReceived: 0,
|
||||
BytesTransmitted: 0,
|
||||
}
|
||||
|
||||
peerMetrics := make([]PeerMetrics, len(src))
|
||||
for i, peer := range src {
|
||||
peerMetrics[i] = *NewPeerMetrics(&peer)
|
||||
|
||||
um.BytesReceived += peer.BytesReceived
|
||||
um.BytesTransmitted += peer.BytesTransmitted
|
||||
}
|
||||
um.PeerMetrics = peerMetrics
|
||||
|
||||
return um
|
||||
}
|
195
internal/app/api/v1/models/models_peer.go
Normal file
195
internal/app/api/v1/models/models_peer.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
const ExpiryDateTimeLayout = "\"2006-01-02\""
|
||||
|
||||
type ExpiryDate struct {
|
||||
*time.Time
|
||||
}
|
||||
|
||||
// UnmarshalJSON will unmarshal using 2006-01-02 layout
|
||||
func (d *ExpiryDate) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 0 || string(b) == "null" || string(b) == "\"\"" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := time.Parse(ExpiryDateTimeLayout, string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !parsed.IsZero() {
|
||||
d.Time = &parsed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON will marshal using 2006-01-02 layout
|
||||
func (d *ExpiryDate) MarshalJSON() ([]byte, error) {
|
||||
if d == nil || d.Time == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
s := d.Format(ExpiryDateTimeLayout)
|
||||
return []byte(s), nil
|
||||
}
|
||||
|
||||
// Peer represents a WireGuard peer entry.
|
||||
type Peer struct {
|
||||
// Identifier is the unique identifier of the peer. It is always equal to the public key of the peer.
|
||||
Identifier string `json:"Identifier" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"required,len=44"`
|
||||
// DisplayName is a nice display name / description for the peer.
|
||||
DisplayName string `json:"DisplayName" example:"My Peer" binding:"omitempty,max=64"`
|
||||
// UserIdentifier is the identifier of the user that owns the peer.
|
||||
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||
// InterfaceIdentifier is the identifier of the interface the peer is linked to.
|
||||
InterfaceIdentifier string `json:"InterfaceIdentifier" binding:"required" example:"wg0"`
|
||||
// Disabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
|
||||
Disabled bool `json:"Disabled" example:"false"`
|
||||
// DisabledReason is the reason why the peer has been disabled.
|
||||
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:"This is a reason why the peer has been disabled."`
|
||||
// ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. An expired peer is not able to connect.
|
||||
ExpiresAt ExpiryDate `json:"ExpiresAt,omitempty" binding:"omitempty,datetime=2006-01-02"`
|
||||
// Notes is a note field for peers.
|
||||
Notes string `json:"Notes" example:"This is a note for the peer."`
|
||||
|
||||
// Endpoint is the endpoint address of the peer.
|
||||
Endpoint ConfigOption[string] `json:"Endpoint"`
|
||||
// EndpointPublicKey is the endpoint public key.
|
||||
EndpointPublicKey ConfigOption[string] `json:"EndpointPublicKey"`
|
||||
// AllowedIPs is a list of allowed IP subnets for the peer.
|
||||
AllowedIPs ConfigOption[[]string] `json:"AllowedIPs"`
|
||||
// ExtraAllowedIPs is a list of additional allowed IP subnets for the peer. These allowed IP subnets are added on the server side.
|
||||
ExtraAllowedIPs []string `json:"ExtraAllowedIPs"`
|
||||
// PresharedKey is the optional pre-shared Key of the peer.
|
||||
PresharedKey string `json:"PresharedKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"omitempty,len=44"`
|
||||
// PersistentKeepalive is the optional persistent keep-alive interval in seconds.
|
||||
PersistentKeepalive ConfigOption[int] `json:"PersistentKeepalive"`
|
||||
|
||||
// PrivateKey is the private Key of the peer.
|
||||
PrivateKey string `json:"PrivateKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"required,len=44"`
|
||||
// PublicKey is the public Key of the server peer.
|
||||
PublicKey string `json:"PublicKey" example:"TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=" binding:"omitempty,len=44"`
|
||||
|
||||
// Mode is the peer interface type (server, client, any).
|
||||
Mode string `json:"Mode" example:"client" binding:"omitempty,oneof=server client any"`
|
||||
|
||||
// Addresses is a list of IP addresses in CIDR format (both IPv4 and IPv6) for the peer.
|
||||
Addresses []string `json:"Addresses" example:"10.11.12.2/24" binding:"omitempty,dive,cidr"`
|
||||
// CheckAliveAddress is an optional ip address or DNS name that is used for ping checks.
|
||||
CheckAliveAddress string `json:"CheckAliveAddress" binding:"omitempty,ip|fqdn" example:"1.1.1.1"`
|
||||
// Dns is a list of DNS servers that should be set if the peer interface is up.
|
||||
Dns ConfigOption[[]string] `json:"Dns"`
|
||||
// DnsSearch is the dns search option string that should be set if the peer interface is up, will be appended to Dns servers.
|
||||
DnsSearch ConfigOption[[]string] `json:"DnsSearch"`
|
||||
// Mtu is the device MTU of the peer.
|
||||
Mtu ConfigOption[int] `json:"Mtu"`
|
||||
// FirewallMark is an optional firewall mark which is used to handle peer traffic.
|
||||
FirewallMark ConfigOption[uint32] `json:"FirewallMark"`
|
||||
// RoutingTable is an optional routing table which is used to route peer traffic.
|
||||
RoutingTable ConfigOption[string] `json:"RoutingTable"`
|
||||
|
||||
// PreUp is an optional action that is executed before the device is up.
|
||||
PreUp ConfigOption[string] `json:"PreUp"`
|
||||
// PostUp is an optional action that is executed after the device is up.
|
||||
PostUp ConfigOption[string] `json:"PostUp"`
|
||||
// PreDown is an optional action that is executed before the device is down.
|
||||
PreDown ConfigOption[string] `json:"PreDown"`
|
||||
// PostDown is an optional action that is executed after the device is down.
|
||||
PostDown ConfigOption[string] `json:"PostDown"`
|
||||
}
|
||||
|
||||
func NewPeer(src *domain.Peer) *Peer {
|
||||
return &Peer{
|
||||
Identifier: string(src.Identifier),
|
||||
DisplayName: src.DisplayName,
|
||||
UserIdentifier: string(src.UserIdentifier),
|
||||
InterfaceIdentifier: string(src.InterfaceIdentifier),
|
||||
Disabled: src.IsDisabled(),
|
||||
DisabledReason: src.DisabledReason,
|
||||
ExpiresAt: ExpiryDate{src.ExpiresAt},
|
||||
Notes: src.Notes,
|
||||
Endpoint: ConfigOptionFromDomain(src.Endpoint),
|
||||
EndpointPublicKey: ConfigOptionFromDomain(src.EndpointPublicKey),
|
||||
AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr),
|
||||
ExtraAllowedIPs: internal.SliceString(src.ExtraAllowedIPsStr),
|
||||
PresharedKey: string(src.PresharedKey),
|
||||
PersistentKeepalive: ConfigOptionFromDomain(src.PersistentKeepalive),
|
||||
PrivateKey: src.Interface.PrivateKey,
|
||||
PublicKey: src.Interface.PublicKey,
|
||||
Mode: string(src.Interface.Type),
|
||||
Addresses: domain.CidrsToStringSlice(src.Interface.Addresses),
|
||||
CheckAliveAddress: src.Interface.CheckAliveAddress,
|
||||
Dns: StringSliceConfigOptionFromDomain(src.Interface.DnsStr),
|
||||
DnsSearch: StringSliceConfigOptionFromDomain(src.Interface.DnsSearchStr),
|
||||
Mtu: ConfigOptionFromDomain(src.Interface.Mtu),
|
||||
FirewallMark: ConfigOptionFromDomain(src.Interface.FirewallMark),
|
||||
RoutingTable: ConfigOptionFromDomain(src.Interface.RoutingTable),
|
||||
PreUp: ConfigOptionFromDomain(src.Interface.PreUp),
|
||||
PostUp: ConfigOptionFromDomain(src.Interface.PostUp),
|
||||
PreDown: ConfigOptionFromDomain(src.Interface.PreDown),
|
||||
PostDown: ConfigOptionFromDomain(src.Interface.PostDown),
|
||||
}
|
||||
}
|
||||
|
||||
func NewPeers(src []domain.Peer) []Peer {
|
||||
results := make([]Peer, len(src))
|
||||
for i := range src {
|
||||
results[i] = *NewPeer(&src[i])
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func NewDomainPeer(src *Peer) *domain.Peer {
|
||||
now := time.Now()
|
||||
|
||||
cidrs, _ := domain.CidrsFromArray(src.Addresses)
|
||||
|
||||
res := &domain.Peer{
|
||||
BaseModel: domain.BaseModel{},
|
||||
Endpoint: ConfigOptionToDomain(src.Endpoint),
|
||||
EndpointPublicKey: ConfigOptionToDomain(src.EndpointPublicKey),
|
||||
AllowedIPsStr: StringSliceConfigOptionToDomain(src.AllowedIPs),
|
||||
ExtraAllowedIPsStr: internal.SliceToString(src.ExtraAllowedIPs),
|
||||
PresharedKey: domain.PreSharedKey(src.PresharedKey),
|
||||
PersistentKeepalive: ConfigOptionToDomain(src.PersistentKeepalive),
|
||||
DisplayName: src.DisplayName,
|
||||
Identifier: domain.PeerIdentifier(src.Identifier),
|
||||
UserIdentifier: domain.UserIdentifier(src.UserIdentifier),
|
||||
InterfaceIdentifier: domain.InterfaceIdentifier(src.InterfaceIdentifier),
|
||||
Disabled: nil, // set below
|
||||
DisabledReason: src.DisabledReason,
|
||||
ExpiresAt: src.ExpiresAt.Time,
|
||||
Notes: src.Notes,
|
||||
Interface: domain.PeerInterfaceConfig{
|
||||
KeyPair: domain.KeyPair{
|
||||
PrivateKey: src.PrivateKey,
|
||||
PublicKey: src.PublicKey,
|
||||
},
|
||||
Type: domain.InterfaceType(src.Mode),
|
||||
Addresses: cidrs,
|
||||
CheckAliveAddress: src.CheckAliveAddress,
|
||||
DnsStr: StringSliceConfigOptionToDomain(src.Dns),
|
||||
DnsSearchStr: StringSliceConfigOptionToDomain(src.DnsSearch),
|
||||
Mtu: ConfigOptionToDomain(src.Mtu),
|
||||
FirewallMark: ConfigOptionToDomain(src.FirewallMark),
|
||||
RoutingTable: ConfigOptionToDomain(src.RoutingTable),
|
||||
PreUp: ConfigOptionToDomain(src.PreUp),
|
||||
PostUp: ConfigOptionToDomain(src.PostUp),
|
||||
PreDown: ConfigOptionToDomain(src.PreDown),
|
||||
PostDown: ConfigOptionToDomain(src.PostDown),
|
||||
},
|
||||
}
|
||||
|
||||
if src.Disabled {
|
||||
res.Disabled = &now
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
75
internal/app/api/v1/models/models_provisioning.go
Normal file
75
internal/app/api/v1/models/models_provisioning.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package models
|
||||
|
||||
import "github.com/h44z/wg-portal/internal/domain"
|
||||
|
||||
// UserInformation represents the information about a user and its linked peers.
|
||||
type UserInformation struct {
|
||||
// UserIdentifier is the unique identifier of the user.
|
||||
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||
// PeerCount is the number of peers linked to the user.
|
||||
PeerCount int `json:"PeerCount" example:"2"`
|
||||
// Peers is a list of peers linked to the user.
|
||||
Peers []UserInformationPeer `json:"Peers"`
|
||||
}
|
||||
|
||||
// UserInformationPeer represents the information about a peer.
|
||||
type UserInformationPeer struct {
|
||||
// Identifier is the unique identifier of the peer. It equals the public key of the peer.
|
||||
Identifier string `json:"Identifier" example:"peer-1234567"`
|
||||
// DisplayName is a user-defined description of the peer.
|
||||
DisplayName string `json:"DisplayName" example:"My iPhone"`
|
||||
// IPAddresses is a list of IP addresses in CIDR format assigned to the peer.
|
||||
IpAddresses []string `json:"IpAddresses" example:"10.11.12.2/24"`
|
||||
// IsDisabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
|
||||
IsDisabled bool `json:"IsDisabled,omitempty" example:"true"`
|
||||
|
||||
// InterfaceIdentifier is the unique identifier of the WireGuard Portal device the peer is connected to.
|
||||
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0"`
|
||||
}
|
||||
|
||||
func NewUserInformation(user *domain.User, peers []domain.Peer) *UserInformation {
|
||||
if user == nil {
|
||||
return &UserInformation{}
|
||||
}
|
||||
|
||||
ui := &UserInformation{
|
||||
UserIdentifier: string(user.Identifier),
|
||||
PeerCount: len(peers),
|
||||
}
|
||||
|
||||
for _, peer := range peers {
|
||||
ui.Peers = append(ui.Peers, NewUserInformationPeer(peer))
|
||||
}
|
||||
|
||||
if len(ui.Peers) == 0 {
|
||||
ui.Peers = []UserInformationPeer{} // Ensure that the JSON output is an empty array instead of null.
|
||||
}
|
||||
|
||||
return ui
|
||||
}
|
||||
|
||||
func NewUserInformationPeer(peer domain.Peer) UserInformationPeer {
|
||||
up := UserInformationPeer{
|
||||
Identifier: string(peer.Identifier),
|
||||
DisplayName: peer.DisplayName,
|
||||
IpAddresses: domain.CidrsToStringSlice(peer.Interface.Addresses),
|
||||
IsDisabled: peer.IsDisabled(),
|
||||
InterfaceIdentifier: string(peer.InterfaceIdentifier),
|
||||
}
|
||||
|
||||
return up
|
||||
}
|
||||
|
||||
// ProvisioningRequest represents a request to provision a new peer.
|
||||
type ProvisioningRequest struct {
|
||||
// InterfaceIdentifier is the identifier of the WireGuard interface the peer should be linked to.
|
||||
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0" binding:"required"`
|
||||
// UserIdentifier is the identifier of the user the peer should be linked to.
|
||||
// If no user identifier is set, the authenticated user is used.
|
||||
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||
|
||||
// PublicKey is the optional public key of the peer. If no public key is set, a new key pair is generated.
|
||||
PublicKey string `json:"PublicKey" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"omitempty,len=44"`
|
||||
// PresharedKey is the optional pre-shared key of the peer. If no pre-shared key is set, a new key is generated.
|
||||
PresharedKey string `json:"PresharedKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"omitempty,len=44"`
|
||||
}
|
125
internal/app/api/v1/models/models_user.go
Normal file
125
internal/app/api/v1/models/models_user.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
// User represents a user in the system.
|
||||
type User struct {
|
||||
// The unique identifier of the user.
|
||||
Identifier string `json:"Identifier" binding:"required,max=64" example:"uid-1234567"`
|
||||
// The email address of the user. This field is optional.
|
||||
Email string `json:"Email" binding:"omitempty,email" example:"test@test.com"`
|
||||
// The source of the user. This field is optional.
|
||||
Source string `json:"Source" binding:"oneof=db" example:"db"`
|
||||
// The name of the authentication provider. This field is read-only.
|
||||
ProviderName string `json:"ProviderName,omitempty" readonly:"true" example:""`
|
||||
// If this field is set, the user is an admin.
|
||||
IsAdmin bool `json:"IsAdmin" binding:"required" example:"false"`
|
||||
|
||||
// The first name of the user. This field is optional.
|
||||
Firstname string `json:"Firstname" example:"Max"`
|
||||
// The last name of the user. This field is optional.
|
||||
Lastname string `json:"Lastname" example:"Muster"`
|
||||
// The phone number of the user. This field is optional.
|
||||
Phone string `json:"Phone" example:"+1234546789"`
|
||||
// The department of the user. This field is optional.
|
||||
Department string `json:"Department" example:"Software Development"`
|
||||
// Additional notes about the user. This field is optional.
|
||||
Notes string `json:"Notes" example:"some sample notes"`
|
||||
|
||||
// The password of the user. This field is never populated on read operations.
|
||||
Password string `json:"Password,omitempty" binding:"omitempty,min=16,max=64" example:""`
|
||||
// If this field is set, the user is disabled.
|
||||
Disabled bool `json:"Disabled" example:"false"`
|
||||
// The reason why the user has been disabled.
|
||||
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:""`
|
||||
// If this field is set, the user is locked and thus unable to log in to WireGuard Portal.
|
||||
Locked bool `json:"Locked" example:"false"`
|
||||
// The reason why the user has been locked.
|
||||
LockedReason string `json:"LockedReason" binding:"required_if=Locked true" example:""`
|
||||
|
||||
// The API token of the user. This field is never populated on bulk read operations.
|
||||
ApiToken string `json:"ApiToken,omitempty" binding:"omitempty,min=32,max=64" example:""`
|
||||
// If this field is set, the user is allowed to use the RESTful API. This field is read-only.
|
||||
ApiEnabled bool `json:"ApiEnabled" readonly:"true" example:"false"`
|
||||
|
||||
// The number of peers linked to the user. This field is read-only.
|
||||
PeerCount int `json:"PeerCount" readonly:"true" example:"2"`
|
||||
}
|
||||
|
||||
func NewUser(src *domain.User, exposeCredentials bool) *User {
|
||||
u := &User{
|
||||
Identifier: string(src.Identifier),
|
||||
Email: src.Email,
|
||||
Source: string(src.Source),
|
||||
ProviderName: src.ProviderName,
|
||||
IsAdmin: src.IsAdmin,
|
||||
Firstname: src.Firstname,
|
||||
Lastname: src.Lastname,
|
||||
Phone: src.Phone,
|
||||
Department: src.Department,
|
||||
Notes: src.Notes,
|
||||
Password: "", // never fill password
|
||||
Disabled: src.IsDisabled(),
|
||||
DisabledReason: src.DisabledReason,
|
||||
Locked: src.IsLocked(),
|
||||
LockedReason: src.LockedReason,
|
||||
ApiToken: "", // by default, do not expose API token
|
||||
ApiEnabled: src.IsApiEnabled(),
|
||||
PeerCount: src.LinkedPeerCount,
|
||||
}
|
||||
|
||||
if exposeCredentials {
|
||||
u.ApiToken = src.ApiToken
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func NewUsers(src []domain.User) []User {
|
||||
results := make([]User, len(src))
|
||||
for i := range src {
|
||||
results[i] = *NewUser(&src[i], false)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func NewDomainUser(src *User) *domain.User {
|
||||
now := time.Now()
|
||||
res := &domain.User{
|
||||
Identifier: domain.UserIdentifier(src.Identifier),
|
||||
Email: src.Email,
|
||||
Source: domain.UserSource(src.Source),
|
||||
ProviderName: src.ProviderName,
|
||||
IsAdmin: src.IsAdmin,
|
||||
Firstname: src.Firstname,
|
||||
Lastname: src.Lastname,
|
||||
Phone: src.Phone,
|
||||
Department: src.Department,
|
||||
Notes: src.Notes,
|
||||
Password: domain.PrivateString(src.Password),
|
||||
Disabled: nil, // set below
|
||||
DisabledReason: src.DisabledReason,
|
||||
Locked: nil, // set below
|
||||
LockedReason: src.LockedReason,
|
||||
}
|
||||
|
||||
if src.ApiToken != "" {
|
||||
res.ApiToken = src.ApiToken
|
||||
res.ApiTokenCreated = &now
|
||||
}
|
||||
|
||||
if src.Disabled {
|
||||
res.Disabled = &now
|
||||
}
|
||||
|
||||
if src.Locked {
|
||||
res.Locked = &now
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
@@ -22,9 +22,19 @@ type App struct {
|
||||
StatisticsCollector
|
||||
ConfigFileManager
|
||||
MailManager
|
||||
ApiV1Manager
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, bus evbus.MessageBus, authenticator Authenticator, users UserManager, wireGuard WireGuardManager, stats StatisticsCollector, cfgFiles ConfigFileManager, mailer MailManager) (*App, error) {
|
||||
func New(
|
||||
cfg *config.Config,
|
||||
bus evbus.MessageBus,
|
||||
authenticator Authenticator,
|
||||
users UserManager,
|
||||
wireGuard WireGuardManager,
|
||||
stats StatisticsCollector,
|
||||
cfgFiles ConfigFileManager,
|
||||
mailer MailManager,
|
||||
) (*App, error) {
|
||||
|
||||
a := &App{
|
||||
Config: cfg,
|
||||
@@ -60,7 +70,7 @@ func New(cfg *config.Config, bus evbus.MessageBus, authenticator Authenticator,
|
||||
}
|
||||
|
||||
func (a *App) Startup(ctx context.Context) error {
|
||||
|
||||
|
||||
a.UserManager.StartBackgroundJobs(ctx)
|
||||
a.StatisticsCollector.StartBackgroundJobs(ctx)
|
||||
a.WireGuardManager.StartBackgroundJobs(ctx)
|
||||
@@ -117,10 +127,10 @@ func (a *App) createDefaultUser(ctx context.Context) error {
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
admin, err := a.CreateUser(ctx, &domain.User{
|
||||
defaultAdmin := &domain.User{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: "system",
|
||||
UpdatedBy: "system",
|
||||
CreatedBy: domain.CtxSystemAdminId,
|
||||
UpdatedBy: domain.CtxSystemAdminId,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
@@ -140,7 +150,16 @@ func (a *App) createDefaultUser(ctx context.Context) error {
|
||||
Locked: nil,
|
||||
LockedReason: "",
|
||||
LinkedPeerCount: 0,
|
||||
})
|
||||
}
|
||||
if a.Config.Core.AdminApiToken != "" {
|
||||
if len(a.Config.Core.AdminApiToken) < 18 {
|
||||
logrus.Warnf("[SECURITY WARNING] admin API token is too short, should be at least 18 characters long")
|
||||
}
|
||||
defaultAdmin.ApiToken = a.Config.Core.AdminApiToken
|
||||
defaultAdmin.ApiTokenCreated = &now
|
||||
}
|
||||
|
||||
admin, err := a.CreateUser(ctx, defaultAdmin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ import (
|
||||
type UserManager interface {
|
||||
GetUser(context.Context, domain.UserIdentifier) (*domain.User, error)
|
||||
RegisterUser(ctx context.Context, user *domain.User) error
|
||||
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
}
|
||||
|
||||
type Authenticator struct {
|
||||
@@ -371,6 +372,11 @@ func (a *Authenticator) processUserInfo(
|
||||
}
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("registration disabled, cannot create missing user: %w", err)
|
||||
default:
|
||||
err = a.updateExternalUser(ctx, user, userInfo, source, provider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
@@ -400,6 +406,9 @@ func (a *Authenticator) registerNewUser(
|
||||
return nil, fmt.Errorf("failed to register new user: %w", err)
|
||||
}
|
||||
|
||||
logrus.Tracef("registered user %s from external authentication provider, admin user: %t",
|
||||
user.Identifier, user.IsAdmin)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -419,4 +428,64 @@ func (a *Authenticator) getAuthenticatorConfig(id string) (interface{}, error) {
|
||||
return nil, fmt.Errorf("no configuration for Authenticator id %s", id)
|
||||
}
|
||||
|
||||
func (a *Authenticator) updateExternalUser(
|
||||
ctx context.Context,
|
||||
existingUser *domain.User,
|
||||
userInfo *domain.AuthenticatorUserInfo,
|
||||
source domain.UserSource,
|
||||
provider string,
|
||||
) error {
|
||||
if existingUser.IsLocked() || existingUser.IsDisabled() {
|
||||
return nil // user is locked or disabled, do not update
|
||||
}
|
||||
|
||||
isChanged := false
|
||||
if existingUser.Email != userInfo.Email {
|
||||
existingUser.Email = userInfo.Email
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.Firstname != userInfo.Firstname {
|
||||
existingUser.Firstname = userInfo.Firstname
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.Lastname != userInfo.Lastname {
|
||||
existingUser.Lastname = userInfo.Lastname
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.Phone != userInfo.Phone {
|
||||
existingUser.Phone = userInfo.Phone
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.Department != userInfo.Department {
|
||||
existingUser.Department = userInfo.Department
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.IsAdmin != userInfo.IsAdmin {
|
||||
existingUser.IsAdmin = userInfo.IsAdmin
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.Source != source {
|
||||
existingUser.Source = source
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.ProviderName != provider {
|
||||
existingUser.ProviderName = provider
|
||||
isChanged = true
|
||||
}
|
||||
|
||||
if !isChanged {
|
||||
return nil // nothing to update
|
||||
}
|
||||
|
||||
_, err := a.users.UpdateUser(ctx, existingUser)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
logrus.Tracef("updated user %s with data from external authentication provider, admin user: %t",
|
||||
existingUser.Identifier, existingUser.IsAdmin)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// endregion oauth authentication
|
||||
|
@@ -2,6 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type LdapAuthenticator struct {
|
||||
@@ -78,7 +80,10 @@ func (l LdapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (map[string]interface{}, error) {
|
||||
func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (
|
||||
map[string]interface{},
|
||||
error,
|
||||
) {
|
||||
conn, err := internal.LdapConnect(l.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to setup connection: %w", err)
|
||||
@@ -109,6 +114,11 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
|
||||
|
||||
users := internal.LdapConvertEntries(sr, &l.cfg.FieldMap)
|
||||
|
||||
if l.cfg.LogUserInfo {
|
||||
contents, _ := json.Marshal(users[0])
|
||||
logrus.Tracef("User info from LDAP source %s for %s: %v", l.GetName(), userId, string(contents))
|
||||
}
|
||||
|
||||
return users[0], nil
|
||||
}
|
||||
|
||||
|
@@ -6,12 +6,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -21,10 +20,16 @@ type PlainOauthAuthenticator struct {
|
||||
userInfoEndpoint string
|
||||
client *http.Client
|
||||
userInfoMapping config.OauthFields
|
||||
userAdminMapping *config.OauthAdminMapping
|
||||
registrationEnabled bool
|
||||
userInfoLogging bool
|
||||
}
|
||||
|
||||
func newPlainOauthAuthenticator(_ context.Context, callbackUrl string, cfg *config.OAuthProvider) (*PlainOauthAuthenticator, error) {
|
||||
func newPlainOauthAuthenticator(
|
||||
_ context.Context,
|
||||
callbackUrl string,
|
||||
cfg *config.OAuthProvider,
|
||||
) (*PlainOauthAuthenticator, error) {
|
||||
var provider = &PlainOauthAuthenticator{}
|
||||
|
||||
provider.name = cfg.ProviderName
|
||||
@@ -44,7 +49,9 @@ func newPlainOauthAuthenticator(_ context.Context, callbackUrl string, cfg *conf
|
||||
}
|
||||
provider.userInfoEndpoint = cfg.UserInfoURL
|
||||
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
||||
provider.userAdminMapping = &cfg.AdminMapping
|
||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||
provider.userInfoLogging = cfg.LogUserInfo
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -65,11 +72,19 @@ func (p PlainOauthAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCo
|
||||
return p.cfg.AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
func (p PlainOauthAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
func (p PlainOauthAuthenticator) Exchange(
|
||||
ctx context.Context,
|
||||
code string,
|
||||
opts ...oauth2.AuthCodeOption,
|
||||
) (*oauth2.Token, error) {
|
||||
return p.cfg.Exchange(ctx, code, opts...)
|
||||
}
|
||||
|
||||
func (p PlainOauthAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, _ string) (map[string]interface{}, error) {
|
||||
func (p PlainOauthAuthenticator) GetUserInfo(
|
||||
ctx context.Context,
|
||||
token *oauth2.Token,
|
||||
_ string,
|
||||
) (map[string]interface{}, error) {
|
||||
req, err := http.NewRequest("GET", p.userInfoEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create user info get request: %w", err)
|
||||
@@ -93,57 +108,13 @@ func (p PlainOauthAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.
|
||||
return nil, fmt.Errorf("failed to parse user info: %w", err)
|
||||
}
|
||||
|
||||
if p.userInfoLogging {
|
||||
logrus.Tracef("User info from OAuth source %s: %v", p.name, string(contents))
|
||||
}
|
||||
|
||||
return userFields, nil
|
||||
}
|
||||
|
||||
func (p PlainOauthAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
||||
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, p.userInfoMapping.IsAdmin, ""))
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, p.userInfoMapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, p.userInfoMapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, p.userInfoMapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, p.userInfoMapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, p.userInfoMapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, p.userInfoMapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func getOauthFieldMapping(f config.OauthFields) config.OauthFields {
|
||||
defaultMap := config.OauthFields{
|
||||
BaseFields: config.BaseFields{
|
||||
UserIdentifier: "sub",
|
||||
Email: "email",
|
||||
Firstname: "given_name",
|
||||
Lastname: "family_name",
|
||||
Phone: "phone",
|
||||
Department: "department",
|
||||
},
|
||||
IsAdmin: "admin_flag",
|
||||
}
|
||||
if f.UserIdentifier != "" {
|
||||
defaultMap.UserIdentifier = f.UserIdentifier
|
||||
}
|
||||
if f.Email != "" {
|
||||
defaultMap.Email = f.Email
|
||||
}
|
||||
if f.Firstname != "" {
|
||||
defaultMap.Firstname = f.Firstname
|
||||
}
|
||||
if f.Lastname != "" {
|
||||
defaultMap.Lastname = f.Lastname
|
||||
}
|
||||
if f.Phone != "" {
|
||||
defaultMap.Phone = f.Phone
|
||||
}
|
||||
if f.Department != "" {
|
||||
defaultMap.Department = f.Department
|
||||
}
|
||||
if f.IsAdmin != "" {
|
||||
defaultMap.IsAdmin = f.IsAdmin
|
||||
}
|
||||
|
||||
return defaultMap
|
||||
return parseOauthUserInfo(p.userInfoMapping, p.userAdminMapping, raw)
|
||||
}
|
||||
|
@@ -2,14 +2,14 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -19,15 +19,22 @@ type OidcAuthenticator struct {
|
||||
verifier *oidc.IDTokenVerifier
|
||||
cfg *oauth2.Config
|
||||
userInfoMapping config.OauthFields
|
||||
userAdminMapping *config.OauthAdminMapping
|
||||
registrationEnabled bool
|
||||
userInfoLogging bool
|
||||
}
|
||||
|
||||
func newOidcAuthenticator(ctx context.Context, callbackUrl string, cfg *config.OpenIDConnectProvider) (*OidcAuthenticator, error) {
|
||||
func newOidcAuthenticator(
|
||||
_ context.Context,
|
||||
callbackUrl string,
|
||||
cfg *config.OpenIDConnectProvider,
|
||||
) (*OidcAuthenticator, error) {
|
||||
var err error
|
||||
var provider = &OidcAuthenticator{}
|
||||
|
||||
provider.name = cfg.ProviderName
|
||||
provider.provider, err = oidc.NewProvider(context.Background(), cfg.BaseUrl) // use new context here, see https://github.com/coreos/go-oidc/issues/339
|
||||
provider.provider, err = oidc.NewProvider(context.Background(),
|
||||
cfg.BaseUrl) // use new context here, see https://github.com/coreos/go-oidc/issues/339
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new oidc provider: %w", err)
|
||||
}
|
||||
@@ -45,7 +52,9 @@ func newOidcAuthenticator(ctx context.Context, callbackUrl string, cfg *config.O
|
||||
Scopes: scopes,
|
||||
}
|
||||
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
||||
provider.userAdminMapping = &cfg.AdminMapping
|
||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||
provider.userInfoLogging = cfg.LogUserInfo
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
@@ -66,11 +75,17 @@ func (o OidcAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCodeOpti
|
||||
return o.cfg.AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (
|
||||
*oauth2.Token,
|
||||
error,
|
||||
) {
|
||||
return o.cfg.Exchange(ctx, code, opts...)
|
||||
}
|
||||
|
||||
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (map[string]interface{}, error) {
|
||||
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (
|
||||
map[string]interface{},
|
||||
error,
|
||||
) {
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, errors.New("token does not contain id_token")
|
||||
@@ -88,20 +103,14 @@ func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token,
|
||||
return nil, fmt.Errorf("failed to parse extra claims: %w", err)
|
||||
}
|
||||
|
||||
if o.userInfoLogging {
|
||||
contents, _ := json.Marshal(tokenFields)
|
||||
logrus.Tracef("User info from OIDC source %s: %v", o.name, string(contents))
|
||||
}
|
||||
|
||||
return tokenFields, nil
|
||||
}
|
||||
|
||||
func (o OidcAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
||||
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, o.userInfoMapping.IsAdmin, ""))
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, o.userInfoMapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, o.userInfoMapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, o.userInfoMapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, o.userInfoMapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, o.userInfoMapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, o.userInfoMapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
return parseOauthUserInfo(o.userInfoMapping, o.userAdminMapping, raw)
|
||||
}
|
||||
|
92
internal/app/auth/oauth_common.go
Normal file
92
internal/app/auth/oauth_common.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
// parseOauthUserInfo parses the raw user info from the oauth provider and maps it to the internal user info struct
|
||||
func parseOauthUserInfo(
|
||||
mapping config.OauthFields,
|
||||
adminMapping *config.OauthAdminMapping,
|
||||
raw map[string]interface{},
|
||||
) (*domain.AuthenticatorUserInfo, error) {
|
||||
var isAdmin bool
|
||||
|
||||
// first try to match the is_admin field against the given regex
|
||||
if mapping.IsAdmin != "" {
|
||||
re := adminMapping.GetAdminValueRegex()
|
||||
if re.MatchString(strings.TrimSpace(internal.MapDefaultString(raw, mapping.IsAdmin, ""))) {
|
||||
isAdmin = true
|
||||
}
|
||||
}
|
||||
|
||||
// next try to parse the user's groups
|
||||
if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" {
|
||||
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
|
||||
re := adminMapping.GetAdminGroupRegex()
|
||||
for _, group := range userGroups {
|
||||
if re.MatchString(strings.TrimSpace(group)) {
|
||||
isAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, mapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, mapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
// getOauthFieldMapping returns the default field mapping for the oauth provider
|
||||
func getOauthFieldMapping(f config.OauthFields) config.OauthFields {
|
||||
defaultMap := config.OauthFields{
|
||||
BaseFields: config.BaseFields{
|
||||
UserIdentifier: "sub",
|
||||
Email: "email",
|
||||
Firstname: "given_name",
|
||||
Lastname: "family_name",
|
||||
Phone: "phone",
|
||||
Department: "department",
|
||||
},
|
||||
IsAdmin: "admin_flag",
|
||||
UserGroups: "", // by default, do not use user groups
|
||||
}
|
||||
if f.UserIdentifier != "" {
|
||||
defaultMap.UserIdentifier = f.UserIdentifier
|
||||
}
|
||||
if f.Email != "" {
|
||||
defaultMap.Email = f.Email
|
||||
}
|
||||
if f.Firstname != "" {
|
||||
defaultMap.Firstname = f.Firstname
|
||||
}
|
||||
if f.Lastname != "" {
|
||||
defaultMap.Lastname = f.Lastname
|
||||
}
|
||||
if f.Phone != "" {
|
||||
defaultMap.Phone = f.Phone
|
||||
}
|
||||
if f.Department != "" {
|
||||
defaultMap.Department = f.Department
|
||||
}
|
||||
if f.IsAdmin != "" {
|
||||
defaultMap.IsAdmin = f.IsAdmin
|
||||
}
|
||||
if f.UserGroups != "" {
|
||||
defaultMap.UserGroups = f.UserGroups
|
||||
}
|
||||
|
||||
return defaultMap
|
||||
}
|
57
internal/app/auth/oauth_common_test.go
Normal file
57
internal/app/auth/oauth_common_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_parseOauthUserInfo(t *testing.T) {
|
||||
userInfoStr := `
|
||||
{
|
||||
"at_hash": "REDACTED",
|
||||
"aud": "REDACTED",
|
||||
"c_hash": "REDACTED",
|
||||
"email": "test@mydomain.net",
|
||||
"email_verified": true,
|
||||
"exp": 1737404259,
|
||||
"groups": [
|
||||
"abuse@mydomain.net",
|
||||
"postmaster@mydomain.net",
|
||||
"wgportal-admins@mydomain.net"
|
||||
],
|
||||
"iat": 1737317859,
|
||||
"iss": "https://dex.mydomain.net",
|
||||
"name": "Test User",
|
||||
"nonce": "REDACTED",
|
||||
"sub": "REDACTED"
|
||||
}
|
||||
`
|
||||
|
||||
userInfo := map[string]interface{}{}
|
||||
err := json.Unmarshal([]byte(userInfoStr), &userInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
fieldMapping := getOauthFieldMapping(config.OauthFields{
|
||||
BaseFields: config.BaseFields{
|
||||
UserIdentifier: "email",
|
||||
Email: "email",
|
||||
Firstname: "name",
|
||||
Lastname: "family_name",
|
||||
},
|
||||
UserGroups: "groups",
|
||||
})
|
||||
adminMapping := &config.OauthAdminMapping{
|
||||
AdminGroupRegex: "^wgportal-admins@mydomain.net$",
|
||||
}
|
||||
|
||||
info, err := parseOauthUserInfo(fieldMapping, adminMapping, userInfo)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info.IsAdmin)
|
||||
assert.Equal(t, info.Firstname, "Test User")
|
||||
assert.Equal(t, info.Lastname, "")
|
||||
assert.Equal(t, info.Email, "test@mydomain.net")
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
package app
|
||||
|
||||
const TopicUserCreated = "user:created"
|
||||
const TopicUserApiEnabled = "user:api:enabled"
|
||||
const TopicUserApiDisabled = "user:api:disabled"
|
||||
const TopicUserRegistered = "user:registered"
|
||||
const TopicUserDisabled = "user:disabled"
|
||||
const TopicUserEnabled = "user:enabled"
|
||||
|
@@ -98,8 +98,8 @@ func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
||||
}
|
||||
newUser := domain.User{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: "v1migrator",
|
||||
UpdatedBy: "v1migrator",
|
||||
CreatedBy: domain.CtxSystemV1Migrator,
|
||||
UpdatedBy: domain.CtxSystemV1Migrator,
|
||||
CreatedAt: oldUser.CreatedAt,
|
||||
UpdatedAt: oldUser.UpdatedAt,
|
||||
},
|
||||
@@ -173,8 +173,8 @@ func migrateV1Interfaces(oldDb, newDb *gorm.DB) error {
|
||||
}
|
||||
newInterface := domain.Interface{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: "v1migrator",
|
||||
UpdatedBy: "v1migrator",
|
||||
CreatedBy: domain.CtxSystemV1Migrator,
|
||||
UpdatedBy: domain.CtxSystemV1Migrator,
|
||||
CreatedAt: oldDevice.CreatedAt,
|
||||
UpdatedAt: oldDevice.UpdatedAt,
|
||||
},
|
||||
@@ -299,8 +299,8 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
|
||||
now := time.Now()
|
||||
user = domain.User{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: "v1migrator",
|
||||
UpdatedBy: "v1migrator",
|
||||
CreatedBy: domain.CtxSystemV1Migrator,
|
||||
UpdatedBy: domain.CtxSystemV1Migrator,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
@@ -322,8 +322,8 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
|
||||
}
|
||||
newPeer := domain.Peer{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: "v1migrator",
|
||||
UpdatedBy: "v1migrator",
|
||||
CreatedBy: domain.CtxSystemV1Migrator,
|
||||
UpdatedBy: domain.CtxSystemV1Migrator,
|
||||
CreatedAt: oldPeer.CreatedAt,
|
||||
UpdatedAt: oldPeer.UpdatedAt,
|
||||
},
|
||||
|
@@ -2,8 +2,9 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"io"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type Authenticator interface {
|
||||
@@ -23,6 +24,8 @@ type UserManager interface {
|
||||
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||
ActivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
}
|
||||
|
||||
type WireGuardManager interface {
|
||||
@@ -36,6 +39,7 @@ type WireGuardManager interface {
|
||||
GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error)
|
||||
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]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)
|
||||
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
|
||||
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||
@@ -43,7 +47,11 @@ type WireGuardManager interface {
|
||||
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
|
||||
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||
CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||
CreateMultiplePeers(ctx context.Context, id domain.InterfaceIdentifier, r *domain.PeerCreationRequest) ([]domain.Peer, error)
|
||||
CreateMultiplePeers(
|
||||
ctx context.Context,
|
||||
id domain.InterfaceIdentifier,
|
||||
r *domain.PeerCreationRequest,
|
||||
) ([]domain.Peer, error)
|
||||
UpdatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||
ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error
|
||||
@@ -63,3 +71,7 @@ type ConfigFileManager interface {
|
||||
type MailManager interface {
|
||||
SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error
|
||||
}
|
||||
|
||||
type ApiV1Manager interface {
|
||||
ApiV1GetUsers(ctx context.Context) ([]domain.User, error)
|
||||
}
|
||||
|
@@ -2,15 +2,21 @@ package users
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func convertRawLdapUser(providerName string, rawUser map[string]any, fields *config.LdapFields, adminGroupDN *ldap.DN) (*domain.User, error) {
|
||||
func convertRawLdapUser(
|
||||
providerName string,
|
||||
rawUser map[string]any,
|
||||
fields *config.LdapFields,
|
||||
adminGroupDN *ldap.DN,
|
||||
) (*domain.User, error) {
|
||||
now := time.Now()
|
||||
|
||||
isAdmin, err := internal.LdapIsMemberOf(rawUser[fields.GroupMembership].([][]byte), adminGroupDN)
|
||||
@@ -20,8 +26,8 @@ func convertRawLdapUser(providerName string, rawUser map[string]any, fields *con
|
||||
|
||||
return &domain.User{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: "ldap_sync",
|
||||
UpdatedBy: "ldap_sync",
|
||||
CreatedBy: domain.CtxSystemLdapSyncer,
|
||||
UpdatedBy: domain.CtxSystemLdapSyncer,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
@@ -65,5 +71,9 @@ func userChangedInLdap(dbUser, ldapUser *domain.User) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if dbUser.ProviderName != ldapUser.ProviderName {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@@ -2,11 +2,13 @@ package users
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type UserDatabaseRepo interface {
|
||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||
FindUsers(ctx context.Context, search string) ([]domain.User, error)
|
||||
SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
@@ -72,11 +73,16 @@ func (m Manager) NewUser(ctx context.Context, user *domain.User) error {
|
||||
u.Identifier = user.Identifier
|
||||
u.Email = user.Email
|
||||
u.Source = user.Source
|
||||
u.ProviderName = user.ProviderName
|
||||
u.IsAdmin = user.IsAdmin
|
||||
u.Firstname = user.Firstname
|
||||
u.Lastname = user.Lastname
|
||||
u.Phone = user.Phone
|
||||
u.Department = user.Department
|
||||
u.Notes = user.Notes
|
||||
u.ApiToken = user.ApiToken
|
||||
u.ApiTokenCreated = user.ApiTokenCreated
|
||||
|
||||
return u, nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -101,7 +107,7 @@ func (m Manager) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain
|
||||
|
||||
user, err := m.users.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load peer %s: %w", id, err)
|
||||
return nil, fmt.Errorf("unable to load user %s: %w", id, err)
|
||||
}
|
||||
peers, _ := m.peers.GetUserPeers(ctx, id) // ignore error, list will be empty in error case
|
||||
|
||||
@@ -110,6 +116,24 @@ func (m Manager) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m Manager) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
|
||||
user, err := m.users.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load user for email %s: %w", email, err)
|
||||
}
|
||||
|
||||
if err := domain.ValidateUserAccessRights(ctx, user.Identifier); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peers, _ := m.peers.GetUserPeers(ctx, user.Identifier) // ignore error, list will be empty in error case
|
||||
|
||||
user.LinkedPeerCount = len(peers)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m Manager) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err
|
||||
@@ -193,7 +217,7 @@ func (m Manager) CreateUser(ctx context.Context, user *domain.User) (*domain.Use
|
||||
return nil, fmt.Errorf("unable to load existing user %s: %w", user.Identifier, err)
|
||||
}
|
||||
if existingUser != nil {
|
||||
return nil, fmt.Errorf("user %s already exists", user.Identifier)
|
||||
return nil, errors.Join(fmt.Errorf("user %s already exists", user.Identifier), domain.ErrDuplicateEntry)
|
||||
}
|
||||
|
||||
if err := m.validateCreation(ctx, user); err != nil {
|
||||
@@ -240,6 +264,59 @@ func (m Manager) DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) ActivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||
user, err := m.users.GetUser(ctx, id)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return nil, fmt.Errorf("unable to find user %s: %w", id, err)
|
||||
}
|
||||
|
||||
if err := m.validateApiChange(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
user.ApiToken = uuid.New().String()
|
||||
user.ApiTokenCreated = &now
|
||||
|
||||
err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||
user.CopyCalculatedAttributes(u)
|
||||
return user, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update failure: %w", err)
|
||||
}
|
||||
|
||||
m.bus.Publish(app.TopicUserApiEnabled, user)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m Manager) DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||
user, err := m.users.GetUser(ctx, id)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return nil, fmt.Errorf("unable to find user %s: %w", id, err)
|
||||
}
|
||||
|
||||
if err := m.validateApiChange(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.ApiToken = ""
|
||||
user.ApiTokenCreated = nil
|
||||
|
||||
err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||
user.CopyCalculatedAttributes(u)
|
||||
return user, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update failure: %w", err)
|
||||
}
|
||||
|
||||
m.bus.Publish(app.TopicUserApiDisabled, user)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m Manager) validateModifications(ctx context.Context, old, new *domain.User) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
@@ -247,28 +324,28 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
}
|
||||
|
||||
if err := old.EditAllowed(new); err != nil {
|
||||
return fmt.Errorf("no access: %w", err)
|
||||
if err := old.EditAllowed(new); err != nil && currentUser.Id != domain.SystemAdminContextUserInfo().Id {
|
||||
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if err := old.CanChangePassword(); err != nil && string(new.Password) != "" {
|
||||
return fmt.Errorf("no access: %w", err)
|
||||
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if currentUser.Id == old.Identifier && old.IsAdmin && !new.IsAdmin {
|
||||
return fmt.Errorf("cannot remove own admin rights")
|
||||
return fmt.Errorf("cannot remove own admin rights: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if currentUser.Id == old.Identifier && new.IsDisabled() {
|
||||
return fmt.Errorf("cannot disable own user")
|
||||
return fmt.Errorf("cannot disable own user: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if currentUser.Id == old.Identifier && new.IsLocked() {
|
||||
return fmt.Errorf("cannot lock own user")
|
||||
return fmt.Errorf("cannot lock own user: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if old.Source != new.Source {
|
||||
return fmt.Errorf("cannot change user source")
|
||||
return fmt.Errorf("cannot change user source: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -282,19 +359,32 @@ func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
|
||||
}
|
||||
|
||||
if new.Identifier == "" {
|
||||
return fmt.Errorf("invalid user identifier")
|
||||
return fmt.Errorf("invalid user identifier: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if new.Identifier == "all" { // the all user identifier collides with the rest api routes
|
||||
return fmt.Errorf("reserved user identifier")
|
||||
if new.Identifier == "all" { // the 'all' user identifier collides with the rest api routes
|
||||
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if new.Identifier == "new" { // the 'new' user identifier collides with the rest api routes
|
||||
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if new.Identifier == "id" { // the 'id' user identifier collides with the rest api routes
|
||||
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if new.Identifier == domain.CtxSystemAdminId || new.Identifier == domain.CtxUnknownUserId {
|
||||
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if new.Source != domain.UserSourceDatabase {
|
||||
return fmt.Errorf("invalid user source: %s", new.Source)
|
||||
return fmt.Errorf("invalid user source: %s, only %s is allowed: %w",
|
||||
new.Source, domain.UserSourceDatabase, domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if string(new.Password) == "" {
|
||||
return fmt.Errorf("invalid password")
|
||||
return fmt.Errorf("invalid password: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -304,15 +394,25 @@ func (m Manager) validateDeletion(ctx context.Context, del *domain.User) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if !currentUser.IsAdmin {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
return domain.ErrNoPermission
|
||||
}
|
||||
|
||||
if err := del.DeleteAllowed(); err != nil {
|
||||
return fmt.Errorf("no access: %w", err)
|
||||
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if currentUser.Id == del.Identifier {
|
||||
return fmt.Errorf("cannot delete own user")
|
||||
return fmt.Errorf("cannot delete own user: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) validateApiChange(ctx context.Context, user *domain.User) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if currentUser.Id != user.Identifier {
|
||||
return fmt.Errorf("cannot change API access of user: %w", domain.ErrNoPermission)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -326,13 +426,14 @@ func (m Manager) runLdapSynchronizationService(ctx context.Context) {
|
||||
logrus.Debugf("sync disabled for LDAP server: %s", cfg.ProviderName)
|
||||
return
|
||||
}
|
||||
|
||||
running := true
|
||||
for running {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
running = false
|
||||
continue
|
||||
case <-time.After(syncInterval * time.Second):
|
||||
case <-time.After(syncInterval):
|
||||
// select blocks until one of the cases evaluate to true
|
||||
}
|
||||
|
||||
@@ -365,10 +466,10 @@ func (m Manager) synchronizeLdapUsers(ctx context.Context, provider *config.Ldap
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Tracef("fetched %d raw ldap users...", len(rawUsers))
|
||||
logrus.Tracef("fetched %d raw ldap users from provider %s...", len(rawUsers), provider.ProviderName)
|
||||
|
||||
// Update existing LDAP users
|
||||
err = m.updateLdapUsers(ctx, provider.ProviderName, rawUsers, &provider.FieldMap, provider.ParsedAdminGroupDN)
|
||||
err = m.updateLdapUsers(ctx, provider, rawUsers, &provider.FieldMap, provider.ParsedAdminGroupDN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -386,13 +487,13 @@ func (m Manager) synchronizeLdapUsers(ctx context.Context, provider *config.Ldap
|
||||
|
||||
func (m Manager) updateLdapUsers(
|
||||
ctx context.Context,
|
||||
providerName string,
|
||||
provider *config.LdapProvider,
|
||||
rawUsers []internal.RawLdapUser,
|
||||
fields *config.LdapFields,
|
||||
adminGroupDN *ldap.DN,
|
||||
) error {
|
||||
for _, rawUser := range rawUsers {
|
||||
user, err := convertRawLdapUser(providerName, rawUser, fields, adminGroupDN)
|
||||
user, err := convertRawLdapUser(provider.ProviderName, rawUser, fields, adminGroupDN)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return fmt.Errorf("failed to convert LDAP data for %v: %w", rawUser["dn"], err)
|
||||
}
|
||||
@@ -402,37 +503,57 @@ func (m Manager) updateLdapUsers(
|
||||
return fmt.Errorf("find error for user id %s: %w", user.Identifier, err)
|
||||
}
|
||||
|
||||
tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
tctx = domain.SetUserInfo(tctx, domain.SystemAdminContextUserInfo())
|
||||
|
||||
// create new user
|
||||
if existingUser == nil {
|
||||
err := m.NewUser(tctx, user)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return fmt.Errorf("create error for user id %s: %w", user.Identifier, err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
if existingUser != nil && existingUser.Source == domain.UserSourceLdap && userChangedInLdap(existingUser,
|
||||
user) {
|
||||
|
||||
// update existing user
|
||||
if provider.AutoReEnable && existingUser.DisabledReason == domain.DisabledReasonLdapMissing {
|
||||
user.Disabled = nil
|
||||
user.DisabledReason = ""
|
||||
} else {
|
||||
user.Disabled = existingUser.Disabled
|
||||
user.DisabledReason = existingUser.DisabledReason
|
||||
}
|
||||
if existingUser.Source == domain.UserSourceLdap && userChangedInLdap(existingUser, user) {
|
||||
err := m.users.SaveUser(tctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||
u.UpdatedAt = time.Now()
|
||||
u.UpdatedBy = "ldap_sync"
|
||||
u.UpdatedBy = domain.CtxSystemLdapSyncer
|
||||
u.Source = user.Source
|
||||
u.ProviderName = user.ProviderName
|
||||
u.Email = user.Email
|
||||
u.Firstname = user.Firstname
|
||||
u.Lastname = user.Lastname
|
||||
u.Phone = user.Phone
|
||||
u.Department = user.Department
|
||||
u.IsAdmin = user.IsAdmin
|
||||
u.Disabled = user.Disabled
|
||||
u.Disabled = nil
|
||||
u.DisabledReason = ""
|
||||
|
||||
return u, nil
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
return fmt.Errorf("update error for user id %s: %w", user.Identifier, err)
|
||||
}
|
||||
|
||||
if existingUser.IsDisabled() && !user.IsDisabled() {
|
||||
m.bus.Publish(app.TopicUserEnabled, *user)
|
||||
}
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -472,10 +593,15 @@ func (m Manager) disableMissingLdapUsers(
|
||||
continue
|
||||
}
|
||||
|
||||
logrus.Tracef("user %s is missing in ldap provider %s, disabling", user.Identifier, providerName)
|
||||
|
||||
now := time.Now()
|
||||
user.Disabled = &now
|
||||
user.DisabledReason = domain.DisabledReasonLdapMissing
|
||||
|
||||
err := m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||
now := time.Now()
|
||||
u.Disabled = &now
|
||||
u.DisabledReason = "missing in ldap"
|
||||
u.Disabled = user.Disabled
|
||||
u.DisabledReason = user.DisabledReason
|
||||
return u, nil
|
||||
})
|
||||
if err != nil {
|
||||
|
@@ -68,6 +68,34 @@ func (m Manager) GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interfa
|
||||
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) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return 0, err
|
||||
@@ -357,7 +385,7 @@ func (m Manager) CreateInterface(ctx context.Context, in *domain.Interface) (*do
|
||||
return nil, fmt.Errorf("unable to load existing interface %s: %w", in.Identifier, err)
|
||||
}
|
||||
if existingInterface != nil {
|
||||
return nil, fmt.Errorf("interface %s already exists", in.Identifier)
|
||||
return nil, fmt.Errorf("interface %s already exists: %w", in.Identifier, domain.ErrDuplicateEntry)
|
||||
}
|
||||
|
||||
if err := m.validateInterfaceCreation(ctx, existingInterface, in); err != nil {
|
||||
@@ -456,6 +484,10 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
|
||||
*domain.Interface,
|
||||
error,
|
||||
) {
|
||||
if err := iface.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("interface validation failed: %w", err)
|
||||
}
|
||||
|
||||
stateChanged := m.hasInterfaceStateChanged(ctx, iface)
|
||||
|
||||
if err := m.handleInterfacePreSaveHooks(stateChanged, iface); err != nil {
|
||||
@@ -705,8 +737,8 @@ func (m Manager) importInterface(ctx context.Context, in *domain.PhysicalInterfa
|
||||
now := time.Now()
|
||||
iface := domain.ConvertPhysicalInterface(in)
|
||||
iface.BaseModel = domain.BaseModel{
|
||||
CreatedBy: "importer",
|
||||
UpdatedBy: "importer",
|
||||
CreatedBy: domain.CtxSystemWgImporter,
|
||||
UpdatedBy: domain.CtxSystemWgImporter,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
@@ -742,8 +774,8 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
|
||||
now := time.Now()
|
||||
peer := domain.ConvertPhysicalPeer(p)
|
||||
peer.BaseModel = domain.BaseModel{
|
||||
CreatedBy: "importer",
|
||||
UpdatedBy: "importer",
|
||||
CreatedBy: domain.CtxSystemWgImporter,
|
||||
UpdatedBy: domain.CtxSystemWgImporter,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
@@ -825,6 +857,13 @@ func (m Manager) validateInterfaceCreation(ctx context.Context, old, new *domain
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
}
|
||||
|
||||
// validate public key if it is set
|
||||
if new.PublicKey != "" && new.PrivateKey != "" {
|
||||
if domain.PublicKeyFromPrivateKey(new.PrivateKey) != new.PublicKey {
|
||||
return fmt.Errorf("invalid public key for given privatekey: %w", domain.ErrInvalidData)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -34,9 +33,9 @@ func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdenti
|
||||
}
|
||||
|
||||
peer.UserIdentifier = userId
|
||||
peer.DisplayName = fmt.Sprintf("Default Peer %s", internal.TruncateString(string(peer.Identifier), 8))
|
||||
peer.Notes = fmt.Sprintf("Default peer created for user %s", userId)
|
||||
peer.AutomaticallyCreated = true
|
||||
peer.GenerateDisplayName("Default")
|
||||
|
||||
newPeers = append(newPeers, *peer)
|
||||
}
|
||||
@@ -63,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) {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err // TODO: self provisioning?
|
||||
if !m.cfg.Core.SelfProvisioningAllowed {
|
||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
@@ -74,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)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get fresh ip addresses: %w", err)
|
||||
@@ -108,7 +113,6 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
|
||||
ExtraAllowedIPsStr: "",
|
||||
PresharedKey: pk,
|
||||
PersistentKeepalive: domain.NewConfigOption(iface.PeerDefPersistentKeepalive, true),
|
||||
DisplayName: fmt.Sprintf("Peer %s", internal.TruncateString(string(peerId), 8)),
|
||||
Identifier: peerId,
|
||||
UserIdentifier: currentUser.Id,
|
||||
InterfaceIdentifier: iface.Identifier,
|
||||
@@ -132,6 +136,7 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
|
||||
PostDown: domain.NewConfigOption(iface.PeerDefPostDown, true),
|
||||
},
|
||||
}
|
||||
freshPeer.GenerateDisplayName("")
|
||||
|
||||
return freshPeer, nil
|
||||
}
|
||||
@@ -150,16 +155,36 @@ func (m Manager) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain
|
||||
}
|
||||
|
||||
func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
|
||||
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||
return nil, err
|
||||
if !m.cfg.Core.SelfProvisioningAllowed {
|
||||
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)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
|
||||
}
|
||||
if existingPeer != nil {
|
||||
return nil, fmt.Errorf("peer %s already exists", peer.Identifier)
|
||||
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 {
|
||||
@@ -230,10 +255,32 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
|
||||
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)
|
||||
if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
|
||||
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier
|
||||
|
||||
// check for already existing peer with new identifier
|
||||
duplicatePeer, err := m.db.GetPeer(ctx, peer.Identifier)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
|
||||
}
|
||||
if duplicatePeer != nil {
|
||||
return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
|
||||
}
|
||||
|
||||
// delete old peer
|
||||
err = m.DeletePeer(ctx, existingPeer.Identifier)
|
||||
if err != nil {
|
||||
@@ -430,8 +477,8 @@ func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interfa
|
||||
func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain.Peer) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if !currentUser.IsAdmin {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
|
||||
return domain.ErrNoPermission
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -441,11 +488,16 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if new.Identifier == "" {
|
||||
return fmt.Errorf("invalid peer identifier")
|
||||
return fmt.Errorf("invalid peer identifier: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
if !currentUser.IsAdmin {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
|
||||
return domain.ErrNoPermission
|
||||
}
|
||||
|
||||
_, err := m.db.GetInterface(ctx, new.InterfaceIdentifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid interface: %w", domain.ErrInvalidData)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -454,8 +506,8 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
|
||||
func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if !currentUser.IsAdmin {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
|
||||
return domain.ErrNoPermission
|
||||
}
|
||||
|
||||
return nil
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user