mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-05 16:06:17 +00:00
Compare commits
60 Commits
v2.0.0-bet
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
9354a1d9d3 | ||
|
e75a32e4d0 | ||
|
1d94f6baaf | ||
|
6681dfa96f | ||
|
a60feb7fc9 | ||
|
37904f96fb | ||
|
1e9ee25e49 | ||
|
30eac7c44a | ||
|
801ce76616 | ||
|
5f9c3bab3e | ||
|
e19f42b1eb | ||
|
34fb373659 | ||
|
b938bc8c4c | ||
|
87bf5da5bd | ||
|
3723e4cc75 | ||
|
6cbccf6d43 | ||
|
a49cfa6343 | ||
|
fe681c015c | ||
|
7d0da4e7ad | ||
|
3218bdd6fb | ||
|
12ccd6e32d | ||
|
02ed7b19df | ||
|
678b6c6456 | ||
|
0206952182 | ||
|
53bae9d194 | ||
|
f616a9f5f4 | ||
|
bf5453c264 | ||
|
fd631d3b9f | ||
|
9680e8350c | ||
|
7473132932 | ||
|
5c51573874 | ||
|
fdb436b135 | ||
|
e24acfa57d | ||
|
10332c7f9a | ||
|
f7d7038829 | ||
|
66ccdc29e9 | ||
|
40b4538e78 | ||
|
986f6fdead | ||
|
dabdf111f9 | ||
|
b074af6dc5 | ||
|
eeb0c87c68 | ||
|
67f076effe | ||
|
f6d7a851d1 | ||
|
fc712ebf42 | ||
|
43163273fa | ||
|
5697c2b7f2 | ||
|
e983a7b8f3 | ||
|
c33eaba1c0 | ||
|
3774257abb | ||
|
588f09bdaa | ||
|
7557a6ef5a | ||
|
3478645317 | ||
|
a950dd76ba | ||
|
8c0ecec485 | ||
|
d01d865b4d | ||
|
1b8cdc3417 | ||
|
d35889de73 | ||
|
0b18b5efd6 | ||
|
2cf2341e4c | ||
|
043d25a08f |
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 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`
|
@@ -20,7 +20,7 @@ RUN npm run build
|
|||||||
######
|
######
|
||||||
# Build backend
|
# Build backend
|
||||||
######
|
######
|
||||||
FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS builder
|
FROM --platform=${BUILDPLATFORM} golang:1.24-alpine AS builder
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
# Download dependencies
|
# Download dependencies
|
||||||
|
275
README.md
275
README.md
@@ -1,261 +1,70 @@
|
|||||||
# WireGuard Portal (v2 - testing)
|
# 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://opensource.org/licenses/MIT)
|
||||||

|

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

|

|
||||||

|

|
||||||
[](https://hub.docker.com/r/wgportal/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
|
> [!CAUTION]
|
||||||
in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
|
> Version 2 is currently under development and may contain bugs and breaking changes.
|
||||||
|
> It is not advised to use this version in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
|
||||||
|
|
||||||
Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to: https://hub.docker.com/r/wgportal/wg-portal.
|
> [!IMPORTANT]
|
||||||
Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
|
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
|
||||||
|
> Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
|
||||||
|
|
||||||
A simple, web based configuration portal for [WireGuard](https://wireguard.com).
|
## Introduction
|
||||||
|
<!-- Text from this line # is included in docs/documentation/overview.md -->
|
||||||
|
**WireGuard Portal** is a simple, web-based configuration portal for [WireGuard](https://wireguard.com) server management.
|
||||||
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
|
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
|
||||||
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
|
interfaces. This allows for the seamless activation or deactivation of new users without disturbing existing VPN
|
||||||
connections.
|
connections.
|
||||||
|
|
||||||
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP (Active Directory or OpenLDAP) as a user source for authentication and profile data.
|
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP
|
||||||
|
(Active Directory or OpenLDAP) as a user source for authentication and profile data.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* Self-hosted - the whole application is a single binary
|
|
||||||
* Responsive web UI written in Vue.JS
|
|
||||||
* Automatically select IP from the network pool assigned to client
|
|
||||||
* QR-Code for convenient mobile client configuration
|
|
||||||
* Sent email to client with QR-code and client config
|
|
||||||
* Enable / Disable clients seamlessly
|
|
||||||
* Generation of wg-quick configuration file (`wgX.conf`) if required
|
|
||||||
* User authentication (database, OAuth or LDAP)
|
|
||||||
* IPv6 ready
|
|
||||||
* Docker ready
|
|
||||||
* Can be used with existing WireGuard setups
|
|
||||||
* Support for multiple WireGuard interfaces
|
|
||||||
* Peer Expiry Feature
|
|
||||||
* Handle route and DNS settings like wg-quick does
|
|
||||||
* Exposes Prometheus [metrics](#metrics)
|
|
||||||
* REST API for management and client deployment
|
|
||||||
|
|
||||||

|
* 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 alerting
|
||||||
|
* REST API for management and client deployment
|
||||||
|
* Webhook for custom actions on peer, interface or user updates
|
||||||
|
|
||||||
|
<!-- Text to this line # is included in docs/documentation/overview.md -->
|
||||||
|

|
||||||
|
|
||||||
## Configuration
|
## Documentation
|
||||||
You can configure WireGuard Portal using a yaml configuration file.
|
|
||||||
The filepath of the yaml configuration file defaults to **config/config.yml** in the working directory of the executable.
|
|
||||||
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
|
|
||||||
For example: `WG_PORTAL_CONFIG=/home/test/config.yml ./wg-portal-amd64`.
|
|
||||||
Also, environment variable substitution in config file is supported. Refer to [syntax](https://github.com/a8m/envsubst?tab=readme-ov-file#docs)
|
|
||||||
|
|
||||||
By default, WireGuard Portal uses a SQLite database. The database is stored in **data/sqlite.db** in the working directory of the executable.
|
For the complete documentation visit [wgportal.org](https://wgportal.org).
|
||||||
|
|
||||||
### Configuration Options
|
|
||||||
The following configuration options are available:
|
|
||||||
|
|
||||||
| configuration key | parent key | default_value | description |
|
|
||||||
|----------------------------------|------------|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. |
|
|
||||||
| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
|
||||||
| editable_keys | core | true | Allow to edit key-pairs in the UI. |
|
|
||||||
| create_default_peer | core | false | If an LDAP user logs in for the first time and has no peers associated, a new WireGuard peer will be created for all server interfaces. |
|
|
||||||
| create_default_peer_on_creation | core | false | If an LDAP user is created (e.g. through LDAP sync), a new WireGuard peer will be created for all server interfaces. |
|
|
||||||
| re_enable_peer_after_user_enable | core | true | Re-enable all peers that were previously disabled due to a user disable action. |
|
|
||||||
| delete_peer_after_user_deleted | core | false | Delete all linked peers if a user gets disabled. Otherwise the peers only get disabled. |
|
|
||||||
| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. |
|
|
||||||
| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. |
|
|
||||||
| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. |
|
|
||||||
| log_level | advanced | info | The loglevel, can be one of: trace, debug, info, warn, error. |
|
|
||||||
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
|
|
||||||
| log_json | advanced | false | Logs in JSON format. |
|
|
||||||
| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. |
|
|
||||||
| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. |
|
|
||||||
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. |
|
|
||||||
| use_ip_v6 | advanced | true | Enable IPv6 support. |
|
|
||||||
| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. |
|
|
||||||
| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. |
|
|
||||||
| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. |
|
|
||||||
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
|
|
||||||
| api_admin_only | advanced | true | This flag specifies if the public REST API is available to administrators only. The API Swagger documentation is available under /api/v1/doc.html |
|
|
||||||
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
|
|
||||||
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
|
|
||||||
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
|
|
||||||
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
|
|
||||||
| data_collection_interval | statistics | 1m | The interval between the data collection cycles. |
|
|
||||||
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
|
|
||||||
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
|
|
||||||
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
|
|
||||||
| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. |
|
|
||||||
| host | mail | 127.0.0.1 | The mail-server address. |
|
|
||||||
| port | mail | 25 | The mail-server SMTP port. |
|
|
||||||
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
|
|
||||||
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
|
|
||||||
| username | mail | | The SMTP user name. |
|
|
||||||
| password | mail | | The SMTP password. |
|
|
||||||
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
|
|
||||||
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
|
|
||||||
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
|
|
||||||
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
|
|
||||||
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
|
|
||||||
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
|
|
||||||
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
|
||||||
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
|
|
||||||
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
|
|
||||||
| client_id | auth/oidc | | The OAuth client id. |
|
|
||||||
| client_secret | auth/oidc | | The OAuth client secret. |
|
|
||||||
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
|
|
||||||
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department, is_admin and user_groups. |
|
|
||||||
| admin_mapping | auth/oidc | | Contains regex values admin_value_regex and admin_group_regex to map the is_admin field and user_groups respectively. |
|
|
||||||
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
|
||||||
| log_user_info | auth/oidc | | If true, the user info retrieved from the OIDC provider will be logged in trace level. |
|
|
||||||
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
|
||||||
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
|
|
||||||
| client_id | auth/oauth | | The OAuth client id. |
|
|
||||||
| client_secret | auth/oauth | | The OAuth client secret. |
|
|
||||||
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
|
|
||||||
| token_url | auth/oauth | | The URL for the token endpoint. |
|
|
||||||
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
|
|
||||||
| scopes | auth/oauth | | OAuth scopes. |
|
|
||||||
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin and user_groups. |
|
|
||||||
| admin_mapping | auth/oauth | | Contains regex values admin_value_regex and admin_group_regex to map the is_admin field and user_groups respectively. |
|
|
||||||
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
|
||||||
| log_user_info | auth/oauth | | If true, the user info retrieved from the OAuth provider will be logged in trace level. |
|
|
||||||
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
|
|
||||||
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
|
|
||||||
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
|
|
||||||
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
|
|
||||||
| tls_key_path | auth/ldap | | A path to the TLS key. |
|
|
||||||
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
|
|
||||||
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
|
|
||||||
| bind_pass | auth/ldap | | The bind password. |
|
|
||||||
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
|
|
||||||
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
|
|
||||||
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
|
|
||||||
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
|
|
||||||
| auto_re_enable | auth/ldap | | If auto re-enable is true, users that where disabled because they were missing will be re-enabled once they are found again. |
|
|
||||||
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
|
|
||||||
| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. |
|
|
||||||
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
|
||||||
| log_user_info | auth/ldap | | If true, the user info retrieved from the LDAP provider will be logged in trace level. |
|
|
||||||
| debug | database | false | Debug database statements (log each statement). |
|
|
||||||
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
|
|
||||||
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
|
|
||||||
| dsn | database | data/sqlite.db | The database DSN. For example: user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local |
|
|
||||||
| request_logging | web | false | Log all HTTP requests. |
|
|
||||||
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
|
|
||||||
| listening_address | web | :8888 | The listening port of the web server. |
|
|
||||||
| session_identifier | web | wgPortalSession | The session identifier for the web frontend. |
|
|
||||||
| session_secret | web | very_secret | The session secret for the web frontend. |
|
|
||||||
| csrf_secret | web | extremely_secret | The CSRF secret. |
|
|
||||||
| site_title | web | WireGuard Portal | The title that is shown in the web frontend. |
|
|
||||||
| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. |
|
|
||||||
| cert_file | web | | (Optional) Path to the TLS certificate file |
|
|
||||||
| key_file | web | | (Optional) Path to the TLS certificate key file |
|
|
||||||
|
|
||||||
A sample config file can be found in the repository: [config.yml.sample](config.yml.sample).
|
|
||||||
More detailed information about the configuration can be found in the [documentation](https://wgportal.org/master/documentation/overview/) on [wgportal.org](https://wgportal.org/master/documentation/overview/).
|
|
||||||
|
|
||||||
|
|
||||||
## Upgrading from V1
|
|
||||||
|
|
||||||
> :warning: Before upgrading from V1, make sure that you have a backup of your currently working configuration files and database!
|
|
||||||
|
|
||||||
To start the upgrade process, start the wg-portal binary with the **-migrateFrom** parameter.
|
|
||||||
The configuration (config.yml) for WireGuard Portal must be updated and valid before starting the upgrade.
|
|
||||||
|
|
||||||
To upgrade from a previous SQLite database, start wg-portal like:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
./wg-portal-amd64 -migrateFrom=old_wg_portal.db
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also specify the database type using the parameter **-migrateFromType**, supported types: mysql, mssql, postgres or sqlite.
|
|
||||||
For example:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom=user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
|
||||||
```
|
|
||||||
|
|
||||||
The upgrade will transform the old, existing database and store the values in the new database specified in config.yml.
|
|
||||||
Ensure that the new database does not contain any data!
|
|
||||||
|
|
||||||
|
|
||||||
## V2 TODOs
|
|
||||||
* Audit UI
|
|
||||||
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
To build a standalone application, use the Makefile provided in the repository.
|
|
||||||
Go version 1.23 or higher has to be installed to build WireGuard Portal.
|
|
||||||
If you want to re-compile the frontend, NodeJS 18 and NPM >= 9 is required.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# build the frontend
|
|
||||||
make frontend
|
|
||||||
|
|
||||||
# build the binary
|
|
||||||
make build
|
|
||||||
```
|
|
||||||
|
|
||||||
## What is out of scope
|
## What is out of scope
|
||||||
* Automatic generation or application of any `iptables` or `nftables` rules.
|
|
||||||
* Support for operating systems other than linux.
|
|
||||||
* Automatic import of private keys of an existing WireGuard setup.
|
|
||||||
|
|
||||||
|
* Automatic generation or application of any `iptables` or `nftables` rules.
|
||||||
|
* Support for operating systems other than linux.
|
||||||
|
* Automatic import of private keys of an existing WireGuard setup.
|
||||||
|
|
||||||
## Application stack
|
## Application stack
|
||||||
|
|
||||||
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
|
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
|
||||||
* [Gin](https://github.com/gin-gonic/gin), HTTP web framework written in Go
|
* [Bootstrap](https://getbootstrap.com/), for the HTML templates
|
||||||
* [Bootstrap](https://getbootstrap.com/), for the HTML templates
|
* [Vue.js](https://vuejs.org/), for the frontend
|
||||||
* [Vue.JS](https://vuejs.org/), for the frontend
|
|
||||||
|
|
||||||
## Metrics
|
|
||||||
|
|
||||||
Metrics are available if interface/peer statistic data collection is enabled.
|
|
||||||
|
|
||||||
Add following scrape job to your Prometheus config file:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# prometheus.yaml
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: "wg-portal"
|
|
||||||
scrape_interval: 60s
|
|
||||||
static_configs:
|
|
||||||
- targets: ["wg-portal:8787"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Exposed metrics:
|
|
||||||
|
|
||||||
```console
|
|
||||||
# HELP wireguard_interface_info Interface info.
|
|
||||||
# TYPE wireguard_interface_info gauge
|
|
||||||
|
|
||||||
# HELP wireguard_interface_received_bytes_total Bytes received througth the interface.
|
|
||||||
# TYPE wireguard_interface_received_bytes_total gauge
|
|
||||||
|
|
||||||
# HELP wireguard_interface_sent_bytes_total Bytes sent through the interface.
|
|
||||||
# TYPE wireguard_interface_sent_bytes_total gauge
|
|
||||||
|
|
||||||
# HELP wireguard_peer_info Peer info.
|
|
||||||
# TYPE wireguard_peer_info gauge
|
|
||||||
|
|
||||||
# HELP wireguard_peer_received_bytes_total Bytes received from the peer.
|
|
||||||
# TYPE wireguard_peer_received_bytes_total gauge
|
|
||||||
|
|
||||||
# HELP wireguard_peer_sent_bytes_total Bytes sent to the peer.
|
|
||||||
# TYPE wireguard_peer_sent_bytes_total gauge
|
|
||||||
|
|
||||||
# HELP wireguard_peer_up Peer connection state (boolean: 1/0).
|
|
||||||
# TYPE wireguard_peer_up gauge
|
|
||||||
|
|
||||||
# HELP wireguard_peer_last_handshake_seconds Seconds from the last handshake with the peer.
|
|
||||||
# TYPE wireguard_peer_last_handshake_seconds gauge
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
* MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT
|
* MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT>
|
||||||
|
12
SECURITY.md
12
SECURITY.md
@@ -4,17 +4,17 @@ If you believe you've found a security issue in one of the supported versions of
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | -------------------- |
|
|---------|--------------------|
|
||||||
| v2.x | :white_check_mark: |
|
| v2.x | :white_check_mark: |
|
||||||
| v1.x | :white_check_mark: |
|
| v1.x | :white_check_mark: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please do not report security vulnerabilities through public GitHub issues.
|
Please do not report security vulnerabilities through public GitHub issues.
|
||||||
|
|
||||||
Instead, we encourage you to submit a report through Github [private vulnerability reporting](https://github.com/h44z/wg-portal/security).
|
Instead, we encourage you to submit a report through GitHub [private vulnerability reporting](https://github.com/h44z/wg-portal/security).
|
||||||
If you prefer to submit a report without logging in to Github, please email *info (at) wgportal.org*.
|
If you prefer to submit a report without logging in to GitHub, please email *info (at) wgportal.org*.
|
||||||
We will respond as soon as possible, but as only two people currently maintain this project, we cannot guarantee specific response times.
|
We will respond as soon as possible, but as only two people currently maintain this project, we cannot guarantee specific response times.
|
||||||
|
|
||||||
We prefer all communications to be in English.
|
We prefer all communications to be in English.
|
||||||
|
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/swaggo/swag"
|
"github.com/swaggo/swag"
|
||||||
"github.com/swaggo/swag/gen"
|
"github.com/swaggo/swag/gen"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var apiRootPath = "/internal/app/api"
|
var apiRootPath = "/internal/app/api"
|
||||||
@@ -100,7 +100,7 @@ func copyDocForMkdocs(workingDir, basePath, version string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func removeAuthorizeButton(input []byte) ([]byte, error) {
|
func removeAuthorizeButton(input []byte) ([]byte, error) {
|
||||||
var swagger map[string]interface{}
|
var swagger map[string]any
|
||||||
err := yaml.Unmarshal(input, &swagger)
|
err := yaml.Unmarshal(input, &swagger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error while unmarshalling swagger file: %w", err)
|
return nil, fmt.Errorf("error while unmarshalling swagger file: %w", err)
|
||||||
|
@@ -2,12 +2,19 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
evbus "github.com/vardius/message-bus"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/adapters"
|
||||||
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
"github.com/h44z/wg-portal/internal/app/api/core"
|
"github.com/h44z/wg-portal/internal/app/api/core"
|
||||||
|
backendV0 "github.com/h44z/wg-portal/internal/app/api/v0/backend"
|
||||||
handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers"
|
handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers"
|
||||||
backendV1 "github.com/h44z/wg-portal/internal/app/api/v1/backend"
|
backendV1 "github.com/h44z/wg-portal/internal/app/api/v1/backend"
|
||||||
handlersV1 "github.com/h44z/wg-portal/internal/app/api/v1/handlers"
|
handlersV1 "github.com/h44z/wg-portal/internal/app/api/v1/handlers"
|
||||||
@@ -17,26 +24,20 @@ import (
|
|||||||
"github.com/h44z/wg-portal/internal/app/mail"
|
"github.com/h44z/wg-portal/internal/app/mail"
|
||||||
"github.com/h44z/wg-portal/internal/app/route"
|
"github.com/h44z/wg-portal/internal/app/route"
|
||||||
"github.com/h44z/wg-portal/internal/app/users"
|
"github.com/h44z/wg-portal/internal/app/users"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/webhooks"
|
||||||
"github.com/h44z/wg-portal/internal/app/wireguard"
|
"github.com/h44z/wg-portal/internal/app/wireguard"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
|
||||||
"github.com/h44z/wg-portal/internal/adapters"
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
evbus "github.com/vardius/message-bus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// main entry point for WireGuard Portal
|
// main entry point for WireGuard Portal
|
||||||
func main() {
|
func main() {
|
||||||
ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
logrus.Infof("Starting WireGuard Portal V2...")
|
slog.Info("Starting WireGuard Portal V2...", "version", internal.Version)
|
||||||
logrus.Infof("WireGuard Portal version: %s", internal.Version)
|
|
||||||
|
|
||||||
cfg, err := config.GetConfig()
|
cfg, err := config.GetConfig()
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
setupLogging(cfg)
|
internal.SetupLogging(cfg.Advanced.LogLevel, cfg.Advanced.LogPretty, cfg.Advanced.LogJson)
|
||||||
|
|
||||||
cfg.LogStartupValues()
|
cfg.LogStartupValues()
|
||||||
|
|
||||||
@@ -57,31 +58,40 @@ func main() {
|
|||||||
cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath)
|
cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
shouldExit, err := app.HandleProgramArgs(cfg, rawDb)
|
shouldExit, err := app.HandleProgramArgs(rawDb)
|
||||||
switch {
|
switch {
|
||||||
case shouldExit && err == nil:
|
case shouldExit && err == nil:
|
||||||
return
|
return
|
||||||
case shouldExit && err != nil:
|
case shouldExit:
|
||||||
logrus.Errorf("Failed to process program args: %v", err)
|
slog.Error("Failed to process program args", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
case !shouldExit:
|
default:
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
queueSize := 100
|
queueSize := 100
|
||||||
eventBus := evbus.New(queueSize)
|
eventBus := evbus.New(queueSize)
|
||||||
|
|
||||||
|
auditManager := audit.NewManager(database)
|
||||||
|
|
||||||
|
auditRecorder, err := audit.NewAuditRecorder(cfg, eventBus, database)
|
||||||
|
internal.AssertNoError(err)
|
||||||
|
auditRecorder.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
userManager, err := users.NewUserManager(cfg, eventBus, database, database)
|
userManager, err := users.NewUserManager(cfg, eventBus, database, database)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
userManager.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
|
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
wireGuardManager.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, eventBus, database, wireGuard, metricsServer)
|
statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, eventBus, database, wireGuard, metricsServer)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
statisticsCollector.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem)
|
cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
@@ -89,35 +99,65 @@ func main() {
|
|||||||
mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database)
|
mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
auditRecorder, err := audit.NewAuditRecorder(cfg, eventBus, database)
|
|
||||||
internal.AssertNoError(err)
|
|
||||||
auditRecorder.StartBackgroundJobs(ctx)
|
|
||||||
|
|
||||||
routeManager, err := route.NewRouteManager(cfg, eventBus, database)
|
routeManager, err := route.NewRouteManager(cfg, eventBus, database)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
routeManager.StartBackgroundJobs(ctx)
|
routeManager.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
backend, err := app.New(cfg, eventBus, authenticator, userManager, wireGuardManager,
|
webhookManager, err := webhooks.NewManager(cfg, eventBus)
|
||||||
statisticsCollector, cfgFileManager, mailManager)
|
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
err = backend.Startup(ctx)
|
webhookManager.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
|
err = app.Initialize(cfg, wireGuardManager, userManager)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
apiFrontend := handlersV0.NewRestApi(cfg, backend)
|
validatorManager := validator.New()
|
||||||
|
|
||||||
|
// region API v0 (SPA frontend)
|
||||||
|
|
||||||
|
apiV0Session := handlersV0.NewSessionWrapper(cfg)
|
||||||
|
apiV0Auth := handlersV0.NewAuthenticationHandler(authenticator, apiV0Session)
|
||||||
|
|
||||||
|
apiV0BackendUsers := backendV0.NewUserService(cfg, userManager, wireGuardManager)
|
||||||
|
apiV0BackendInterfaces := backendV0.NewInterfaceService(cfg, wireGuardManager, cfgFileManager)
|
||||||
|
apiV0BackendPeers := backendV0.NewPeerService(cfg, wireGuardManager, cfgFileManager, mailManager)
|
||||||
|
|
||||||
|
apiV0EndpointAuth := handlersV0.NewAuthEndpoint(cfg, apiV0Auth, apiV0Session, validatorManager, authenticator)
|
||||||
|
apiV0EndpointAudit := handlersV0.NewAuditEndpoint(cfg, apiV0Auth, auditManager)
|
||||||
|
apiV0EndpointUsers := handlersV0.NewUserEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendUsers)
|
||||||
|
apiV0EndpointInterfaces := handlersV0.NewInterfaceEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendInterfaces)
|
||||||
|
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
|
||||||
|
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth)
|
||||||
|
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
|
||||||
|
|
||||||
|
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
||||||
|
apiV0EndpointAuth,
|
||||||
|
apiV0EndpointAudit,
|
||||||
|
apiV0EndpointUsers,
|
||||||
|
apiV0EndpointInterfaces,
|
||||||
|
apiV0EndpointPeers,
|
||||||
|
apiV0EndpointConfig,
|
||||||
|
apiV0EndpointTest,
|
||||||
|
)
|
||||||
|
|
||||||
|
// endregion API v0 (SPA frontend)
|
||||||
|
|
||||||
|
// region API v1 (User REST API)
|
||||||
|
|
||||||
|
apiV1Auth := handlersV1.NewAuthenticationHandler(userManager)
|
||||||
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
|
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
|
||||||
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
|
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
|
||||||
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
|
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
|
||||||
apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager)
|
apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager)
|
||||||
apiV1BackendMetrics := backendV1.NewMetricsService(cfg, database, userManager, wireGuardManager)
|
apiV1BackendMetrics := backendV1.NewMetricsService(cfg, database, userManager, wireGuardManager)
|
||||||
apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1BackendUsers)
|
|
||||||
apiV1EndpointPeers := handlersV1.NewPeerEndpoint(apiV1BackendPeers)
|
apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1Auth, validatorManager, apiV1BackendUsers)
|
||||||
apiV1EndpointInterfaces := handlersV1.NewInterfaceEndpoint(apiV1BackendInterfaces)
|
apiV1EndpointPeers := handlersV1.NewPeerEndpoint(apiV1Auth, validatorManager, apiV1BackendPeers)
|
||||||
apiV1EndpointProvisioning := handlersV1.NewProvisioningEndpoint(apiV1BackendProvisioning)
|
apiV1EndpointInterfaces := handlersV1.NewInterfaceEndpoint(apiV1Auth, validatorManager, apiV1BackendInterfaces)
|
||||||
apiV1EndpointMetrics := handlersV1.NewMetricsEndpoint(apiV1BackendMetrics)
|
apiV1EndpointProvisioning := handlersV1.NewProvisioningEndpoint(apiV1Auth, validatorManager,
|
||||||
|
apiV1BackendProvisioning)
|
||||||
|
apiV1EndpointMetrics := handlersV1.NewMetricsEndpoint(apiV1Auth, validatorManager, apiV1BackendMetrics)
|
||||||
|
|
||||||
apiV1 := handlersV1.NewRestApi(
|
apiV1 := handlersV1.NewRestApi(
|
||||||
userManager,
|
|
||||||
apiV1EndpointUsers,
|
apiV1EndpointUsers,
|
||||||
apiV1EndpointPeers,
|
apiV1EndpointPeers,
|
||||||
apiV1EndpointInterfaces,
|
apiV1EndpointInterfaces,
|
||||||
@@ -125,47 +165,22 @@ func main() {
|
|||||||
apiV1EndpointMetrics,
|
apiV1EndpointMetrics,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// endregion API v1 (User REST API)
|
||||||
|
|
||||||
webSrv, err := core.NewServer(cfg, apiFrontend, apiV1)
|
webSrv, err := core.NewServer(cfg, apiFrontend, apiV1)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
go metricsServer.Run(ctx)
|
go metricsServer.Run(ctx)
|
||||||
go webSrv.Run(ctx, cfg.Web.ListeningAddress)
|
go webSrv.Run(ctx, cfg.Web.ListeningAddress)
|
||||||
|
|
||||||
|
slog.Info("Application startup complete")
|
||||||
|
|
||||||
// wait until context gets cancelled
|
// wait until context gets cancelled
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|
||||||
logrus.Infof("Stopping WireGuard Portal")
|
slog.Info("Stopping WireGuard Portal")
|
||||||
|
|
||||||
time.Sleep(5 * time.Second) // wait for (most) goroutines to finish gracefully
|
time.Sleep(5 * time.Second) // wait for (most) goroutines to finish gracefully
|
||||||
|
|
||||||
logrus.Infof("Stopped WireGuard Portal")
|
slog.Info("Stopped WireGuard Portal")
|
||||||
}
|
|
||||||
|
|
||||||
func setupLogging(cfg *config.Config) {
|
|
||||||
switch strings.ToLower(cfg.Advanced.LogLevel) {
|
|
||||||
case "trace":
|
|
||||||
logrus.SetLevel(logrus.TraceLevel)
|
|
||||||
case "debug":
|
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
|
||||||
case "info", "information":
|
|
||||||
logrus.SetLevel(logrus.InfoLevel)
|
|
||||||
case "warn", "warning":
|
|
||||||
logrus.SetLevel(logrus.WarnLevel)
|
|
||||||
case "error":
|
|
||||||
logrus.SetLevel(logrus.ErrorLevel)
|
|
||||||
default:
|
|
||||||
logrus.SetLevel(logrus.InfoLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case cfg.Advanced.LogJson:
|
|
||||||
logrus.SetFormatter(&logrus.JSONFormatter{
|
|
||||||
PrettyPrint: cfg.Advanced.LogPretty,
|
|
||||||
})
|
|
||||||
case cfg.Advanced.LogPretty:
|
|
||||||
logrus.SetFormatter(&logrus.TextFormatter{
|
|
||||||
ForceColors: true,
|
|
||||||
DisableColors: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -13,11 +13,15 @@ web:
|
|||||||
external_url: http://localhost:8888
|
external_url: http://localhost:8888
|
||||||
request_logging: true
|
request_logging: true
|
||||||
|
|
||||||
|
webhook:
|
||||||
|
url: ""
|
||||||
|
authentication: ""
|
||||||
|
timeout: 10s
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
ldap:
|
ldap:
|
||||||
- id: ldap1
|
- id: ldap1
|
||||||
provider_name: company ldap
|
provider_name: company ldap
|
||||||
display_name: Login with</br>LDAP
|
|
||||||
url: ldap://ldap.yourcompany.local:389
|
url: ldap://ldap.yourcompany.local:389
|
||||||
bind_user: ldap_wireguard@yourcompany.local
|
bind_user: ldap_wireguard@yourcompany.local
|
||||||
bind_pass: super_Secret_PASSWORD
|
bind_pass: super_Secret_PASSWORD
|
||||||
|
@@ -2,10 +2,10 @@ apiVersion: v2
|
|||||||
name: wg-portal
|
name: wg-portal
|
||||||
description: WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
description: WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
||||||
# Version is set to ensure compatibility with the chart's Ingress resource.
|
# Version is set to ensure compatibility with the chart's Ingress resource.
|
||||||
kubeVersion: '>=1.19.0'
|
kubeVersion: ">=1.19.0"
|
||||||
type: application
|
type: application
|
||||||
home: https://wgportal.org
|
home: https://wgportal.org
|
||||||
icon: https://wgportal.org/assets/images/logo.svg
|
icon: https://wgportal.org/latest/assets/images/logo.svg
|
||||||
sources:
|
sources:
|
||||||
- https://github.com/h44z/wg-portal
|
- https://github.com/h44z/wg-portal
|
||||||
|
|
||||||
@@ -16,10 +16,10 @@ annotations:
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.5.0
|
version: 0.7.1
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: latest
|
appVersion: "v2"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# wg-portal
|
# wg-portal
|
||||||
|
|
||||||
  
|
  
|
||||||
|
|
||||||
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
||||||
|
|
||||||
@@ -32,13 +32,13 @@ The [Values](#values) section lists the parameters that can be configured during
|
|||||||
| nameOverride | string | `""` | Partially override resource names (adds suffix) |
|
| nameOverride | string | `""` | Partially override resource names (adds suffix) |
|
||||||
| fullnameOverride | string | `""` | Fully override resource names |
|
| fullnameOverride | string | `""` | Fully override resource names |
|
||||||
| extraDeploy | list | `[]` | Array of extra objects to deploy with the release |
|
| extraDeploy | list | `[]` | Array of extra objects to deploy with the release |
|
||||||
| config.advanced | tpl/object | `{}` | Advanced configuration options. |
|
| config.advanced | tpl/object | `{}` | [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options. |
|
||||||
| config.auth | tpl/object | `{}` | Auth configuration options. |
|
| config.auth | tpl/object | `{}` | [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options. |
|
||||||
| config.core | tpl/object | `{}` | Core configuration options.<br> If external admins in `auth` are not defined and there are no `admin_user` and `admin_password` defined here, the default credentials will be generated. |
|
| config.core | tpl/object | `{}` | [Core configuration](https://wgportal.org/latest/documentation/configuration/overview/#core) options.<br> If external admins in `auth` are defined and there are no `admin_user` and `admin_password` defined here, the default admin account will be disabled. |
|
||||||
| config.database | tpl/object | `{}` | Database configuration options |
|
| config.database | tpl/object | `{}` | [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options |
|
||||||
| config.mail | tpl/object | `{}` | Mail configuration options |
|
| config.mail | tpl/object | `{}` | [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options |
|
||||||
| config.statistics | tpl/object | `{}` | Statistics configuration options |
|
| config.statistics | tpl/object | `{}` | [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options |
|
||||||
| config.web | tpl/object | `{}` | Web configuration options.<br> `listening_address` will be set automatically from `service.web.port`. `external_url` is required to enable ingress and certificate resources. |
|
| config.web | tpl/object | `{}` | [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br> `listening_address` will be set automatically from `service.web.port`. `external_url` is required to enable ingress and certificate resources. |
|
||||||
| revisionHistoryLimit | string | `10` | The number of old ReplicaSets to retain to allow rollback. |
|
| revisionHistoryLimit | string | `10` | The number of old ReplicaSets to retain to allow rollback. |
|
||||||
| workloadType | string | `"Deployment"` | Workload type - `Deployment` or `StatefulSet` |
|
| workloadType | string | `"Deployment"` | Workload type - `Deployment` or `StatefulSet` |
|
||||||
| strategy | object | `{"type":"RollingUpdate"}` | Update strategy for the workload Valid values are: `RollingUpdate` or `Recreate` for Deployment, `RollingUpdate` or `OnDelete` for StatefulSet |
|
| strategy | object | `{"type":"RollingUpdate"}` | Update strategy for the workload Valid values are: `RollingUpdate` or `Recreate` for Deployment, `RollingUpdate` or `OnDelete` for StatefulSet |
|
||||||
@@ -73,6 +73,7 @@ The [Values](#values) section lists the parameters that can be configured during
|
|||||||
| service.web.annotations | object | `{}` | Annotations for the web service |
|
| service.web.annotations | object | `{}` | Annotations for the web service |
|
||||||
| service.web.type | string | `"ClusterIP"` | Web service type |
|
| service.web.type | string | `"ClusterIP"` | Web service type |
|
||||||
| service.web.port | int | `8888` | Web service port Used for the web interface listener |
|
| service.web.port | int | `8888` | Web service port Used for the web interface listener |
|
||||||
|
| service.web.appProtocol | string | `"http"` | Web service appProtocol. Will be auto set to `https` if certificate is enabled. |
|
||||||
| service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service |
|
| service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service |
|
||||||
| service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type |
|
| service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type |
|
||||||
| service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. |
|
| service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. |
|
||||||
@@ -81,7 +82,7 @@ The [Values](#values) section lists the parameters that can be configured during
|
|||||||
| ingress.className | string | `""` | Ingress class name |
|
| ingress.className | string | `""` | Ingress class name |
|
||||||
| ingress.annotations | object | `{}` | Ingress annotations |
|
| ingress.annotations | object | `{}` | Ingress annotations |
|
||||||
| ingress.tls | bool | `false` | Ingress TLS configuration. Enable certificate resource or add ingress annotation to create required secret |
|
| ingress.tls | bool | `false` | Ingress TLS configuration. Enable certificate resource or add ingress annotation to create required secret |
|
||||||
| certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created |
|
| certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created. If enabled, certificate will be used for the web. |
|
||||||
| certificate.issuer.name | string | `""` | Certificate issuer name |
|
| certificate.issuer.name | string | `""` | Certificate issuer name |
|
||||||
| certificate.issuer.kind | string | `""` | Certificate issuer kind (ClusterIssuer or Issuer) |
|
| certificate.issuer.kind | string | `""` | Certificate issuer kind (ClusterIssuer or Issuer) |
|
||||||
| certificate.issuer.group | string | `"cert-manager.io"` | Certificate issuer group |
|
| certificate.issuer.group | string | `"cert-manager.io"` | Certificate issuer group |
|
||||||
@@ -101,6 +102,7 @@ The [Values](#values) section lists the parameters that can be configured during
|
|||||||
| persistence.storageClass | string | `""` | Persistent Volume storage class. If undefined (the default) cluster's default provisioner will be used. |
|
| 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.accessMode | string | `"ReadWriteOnce"` | Persistent Volume Access Mode |
|
||||||
| persistence.size | string | `"1Gi"` | Persistent Volume size |
|
| persistence.size | string | `"1Gi"` | Persistent Volume size |
|
||||||
|
| persistence.volumeName | string | `""` | Persistent Volume Name (optional) |
|
||||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||||
| serviceAccount.annotations | object | `{}` | Service account annotations |
|
| serviceAccount.annotations | object | `{}` | Service account annotations |
|
||||||
| serviceAccount.automount | bool | `false` | Automatically mount a ServiceAccount's API credentials |
|
| serviceAccount.automount | bool | `false` | Automatically mount a ServiceAccount's API credentials |
|
||||||
|
@@ -62,9 +62,9 @@ Create the name of the service account to use
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Define default admin credentials
|
Disables default admin credentials
|
||||||
If external auth is enabled and has admin group mappings,
|
If external auth is enabled and has admin group mappings,
|
||||||
the admin_user and admin_password values are not used.
|
the admin_user will be set to blank (disabled).
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "wg-portal.admin" -}}
|
{{- define "wg-portal.admin" -}}
|
||||||
{{- $externalAdmin := false -}}
|
{{- $externalAdmin := false -}}
|
||||||
@@ -80,9 +80,8 @@ the admin_user and admin_password values are not used.
|
|||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- if not $externalAdmin -}}
|
{{- if $externalAdmin -}}
|
||||||
admin_user: admin@wgportal.local
|
admin_user: ""
|
||||||
admin_password: {{ printf "%s/%s" .Release.Name .Release.Namespace | b64enc }}
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
@@ -90,13 +89,17 @@ admin_password: {{ printf "%s/%s" .Release.Name .Release.Namespace | b64enc }}
|
|||||||
Define PersistentVolumeClaim spec
|
Define PersistentVolumeClaim spec
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "wg-portal.pvc" -}}
|
{{- define "wg-portal.pvc" -}}
|
||||||
accessModes: [{{ .Values.persistence.accessMode }}]
|
accessModes:
|
||||||
{{- with .Values.persistence.storageClass }}
|
- {{ .Values.persistence.accessMode }}
|
||||||
storageClassName: {{ . }}
|
|
||||||
{{- end }}
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.persistence.size | quote }}
|
storage: {{ .Values.persistence.size | quote }}
|
||||||
|
{{- with .Values.persistence.storageClass }}
|
||||||
|
storageClassName: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.persistence.volumeName }}
|
||||||
|
volumeName: {{ . }}
|
||||||
|
{{- end }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
|
@@ -51,3 +51,16 @@ spec:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
selector: {{- include "wg-portal.selectorLabels" .context | nindent 4 }}
|
selector: {{- include "wg-portal.selectorLabels" .context | nindent 4 }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Define the service port template for the web port
|
||||||
|
*/}}
|
||||||
|
{{- define "wg-portal.service.webPort" -}}
|
||||||
|
name: web
|
||||||
|
port: {{ .Values.service.web.port }}
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: web
|
||||||
|
{{- if semverCompare ">=1.20-0" .Capabilities.KubeVersion.Version }}
|
||||||
|
appProtocol: {{ ternary "https" .Values.service.web.appProtocol .Values.certificate.enabled }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
@@ -1,3 +1,11 @@
|
|||||||
|
{{- $advanced := dict "start_listen_port" (.Values.service.wireguard.ports | sortAlpha | first | int) -}}
|
||||||
|
{{- $statistics := dict "listening_address" (printf ":%v" .Values.service.metrics.port) -}}
|
||||||
|
{{- $web:= dict "listening_address" (printf ":%v" .Values.service.web.port) -}}
|
||||||
|
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) }}
|
||||||
|
{{- $_ := set $web "cert_file" "/app/certs/tls.crt" }}
|
||||||
|
{{- $_ := set $web "key_file" "/app/certs/tls.key" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
@@ -5,11 +13,9 @@ metadata:
|
|||||||
labels: {{- include "wg-portal.labels" . | nindent 4 }}
|
labels: {{- include "wg-portal.labels" . | nindent 4 }}
|
||||||
stringData:
|
stringData:
|
||||||
config.yml: |
|
config.yml: |
|
||||||
advanced:
|
{{- with mustMerge $advanced .Values.config.advanced }}
|
||||||
start_listen_port: {{ .Values.service.wireguard.ports | sortAlpha | first }}
|
advanced: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||||
{{- with .Values.config.advanced }}
|
{{- end }}
|
||||||
{{- tpl (toYaml (omit . "start_listen_port")) $ | nindent 6 }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- with .Values.config.auth }}
|
{{- with .Values.config.auth }}
|
||||||
auth: {{- tpl (toYaml .) $ | nindent 6 }}
|
auth: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||||
@@ -27,14 +33,10 @@ stringData:
|
|||||||
mail: {{- tpl (toYaml .) $ | nindent 6 }}
|
mail: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
statistics:
|
{{- with mustMerge $statistics .Values.config.statistics }}
|
||||||
listening_address: :{{ .Values.service.metrics.port }}
|
statistics: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||||
{{- with .Values.config.statistics }}
|
{{- end }}
|
||||||
{{- tpl (toYaml (omit . "listening_address")) $ | nindent 6 }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
web:
|
{{- with mustMerge $web .Values.config.web }}
|
||||||
listening_address: :{{ .Values.service.web.port }}
|
web: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||||
{{- with .Values.config.web }}
|
{{- end }}
|
||||||
{{- tpl (toYaml (omit . "listening_address")) $ | nindent 6 }}
|
|
||||||
{{- end }}
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
{{- $portsWeb := list (dict "name" "web" "port" .Values.service.web.port "protocol" "TCP" "targetPort" "web") -}}
|
{{- $portsWeb := list (include "wg-portal.service.webPort" . | fromYaml) -}}
|
||||||
{{- $ports := list -}}
|
{{- $ports := list -}}
|
||||||
{{- range $idx, $port := .Values.service.wireguard.ports -}}
|
{{- range $idx, $port := .Values.service.wireguard.ports -}}
|
||||||
{{- $name := printf "wg%d" $idx -}}
|
{{- $name := printf "wg%d" $idx -}}
|
||||||
|
@@ -3,37 +3,36 @@
|
|||||||
# Declare variables to be passed into your templates.
|
# Declare variables to be passed into your templates.
|
||||||
|
|
||||||
# -- Partially override resource names (adds suffix)
|
# -- Partially override resource names (adds suffix)
|
||||||
nameOverride: ''
|
nameOverride: ""
|
||||||
# -- Fully override resource names
|
# -- Fully override resource names
|
||||||
fullnameOverride: ''
|
fullnameOverride: ""
|
||||||
# -- Array of extra objects to deploy with the release
|
# -- Array of extra objects to deploy with the release
|
||||||
extraDeploy: []
|
extraDeploy: []
|
||||||
|
|
||||||
# https://github.com/h44z/wg-portal/blob/master/README.md#configuration-options
|
|
||||||
config:
|
config:
|
||||||
# -- (tpl/object) Advanced configuration options.
|
# -- (tpl/object) [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options.
|
||||||
advanced: {}
|
advanced: {}
|
||||||
# -- (tpl/object) Auth configuration options.
|
# -- (tpl/object) [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options.
|
||||||
auth: {}
|
auth: {}
|
||||||
# -- (tpl/object) Core configuration options.<br>
|
# -- (tpl/object) [Core configuration](https://wgportal.org/latest/documentation/configuration/overview/#core) options.<br>
|
||||||
# If external admins in `auth` are not defined and
|
# If external admins in `auth` are defined and
|
||||||
# there are no `admin_user` and `admin_password` defined here,
|
# there are no `admin_user` and `admin_password` defined here,
|
||||||
# the default credentials will be generated.
|
# the default admin account will be disabled.
|
||||||
core: {}
|
core: {}
|
||||||
# -- (tpl/object) Database configuration options
|
# -- (tpl/object) [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options
|
||||||
database: {}
|
database: {}
|
||||||
# -- (tpl/object) Mail configuration options
|
# -- (tpl/object) [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options
|
||||||
mail: {}
|
mail: {}
|
||||||
# -- (tpl/object) Statistics configuration options
|
# -- (tpl/object) [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options
|
||||||
statistics: {}
|
statistics: {}
|
||||||
# -- (tpl/object) Web configuration options.<br>
|
# -- (tpl/object) [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br>
|
||||||
# `listening_address` will be set automatically from `service.web.port`.
|
# `listening_address` will be set automatically from `service.web.port`.
|
||||||
# `external_url` is required to enable ingress and certificate resources.
|
# `external_url` is required to enable ingress and certificate resources.
|
||||||
web: {}
|
web: {}
|
||||||
|
|
||||||
# -- The number of old ReplicaSets to retain to allow rollback.
|
# -- The number of old ReplicaSets to retain to allow rollback.
|
||||||
# @default -- `10`
|
# @default -- `10`
|
||||||
revisionHistoryLimit: ''
|
revisionHistoryLimit: ""
|
||||||
# -- Workload type - `Deployment` or `StatefulSet`
|
# -- Workload type - `Deployment` or `StatefulSet`
|
||||||
workloadType: Deployment
|
workloadType: Deployment
|
||||||
# -- Update strategy for the workload
|
# -- Update strategy for the workload
|
||||||
@@ -49,7 +48,7 @@ image:
|
|||||||
# -- Image pull policy
|
# -- Image pull policy
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
# -- Overrides the image tag whose default is the chart appVersion
|
# -- Overrides the image tag whose default is the chart appVersion
|
||||||
tag: ''
|
tag: ""
|
||||||
|
|
||||||
# -- Image pull secrets
|
# -- Image pull secrets
|
||||||
imagePullSecrets: []
|
imagePullSecrets: []
|
||||||
@@ -73,14 +72,14 @@ sidecarContainers: []
|
|||||||
# -- Set DNS policy for the pod.
|
# -- Set DNS policy for the pod.
|
||||||
# Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`.
|
# Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`.
|
||||||
# @default -- `"ClusterFirst"`
|
# @default -- `"ClusterFirst"`
|
||||||
dnsPolicy: ''
|
dnsPolicy: ""
|
||||||
# -- Restart policy for all containers within the pod.
|
# -- Restart policy for all containers within the pod.
|
||||||
# Valid values are `Always`, `OnFailure` or `Never`.
|
# Valid values are `Always`, `OnFailure` or `Never`.
|
||||||
# @default -- `"Always"`
|
# @default -- `"Always"`
|
||||||
restartPolicy: ''
|
restartPolicy: ""
|
||||||
# -- Use the host's network namespace.
|
# -- Use the host's network namespace.
|
||||||
# @default -- `false`.
|
# @default -- `false`.
|
||||||
hostNetwork: ''
|
hostNetwork: ""
|
||||||
# -- Resources requests and limits
|
# -- Resources requests and limits
|
||||||
resources: {}
|
resources: {}
|
||||||
# -- Overwrite pod command
|
# -- Overwrite pod command
|
||||||
@@ -123,6 +122,8 @@ service:
|
|||||||
# -- Web service port
|
# -- Web service port
|
||||||
# Used for the web interface listener
|
# Used for the web interface listener
|
||||||
port: 8888
|
port: 8888
|
||||||
|
# -- Web service appProtocol. Will be auto set to `https` if certificate is enabled.
|
||||||
|
appProtocol: http
|
||||||
wireguard:
|
wireguard:
|
||||||
# -- Annotations for the WireGuard service
|
# -- Annotations for the WireGuard service
|
||||||
annotations: {}
|
annotations: {}
|
||||||
@@ -141,7 +142,7 @@ ingress:
|
|||||||
# -- Specifies whether an ingress resource should be created
|
# -- Specifies whether an ingress resource should be created
|
||||||
enabled: false
|
enabled: false
|
||||||
# -- Ingress class name
|
# -- Ingress class name
|
||||||
className: ''
|
className: ""
|
||||||
# -- Ingress annotations
|
# -- Ingress annotations
|
||||||
annotations: {}
|
annotations: {}
|
||||||
# -- Ingress TLS configuration.
|
# -- Ingress TLS configuration.
|
||||||
@@ -149,21 +150,22 @@ ingress:
|
|||||||
tls: false
|
tls: false
|
||||||
|
|
||||||
certificate:
|
certificate:
|
||||||
# -- Specifies whether a certificate resource should be created
|
# -- Specifies whether a certificate resource should be created.
|
||||||
|
# If enabled, certificate will be used for the web.
|
||||||
enabled: false
|
enabled: false
|
||||||
issuer:
|
issuer:
|
||||||
# -- Certificate issuer name
|
# -- Certificate issuer name
|
||||||
name: ''
|
name: ""
|
||||||
# -- Certificate issuer kind (ClusterIssuer or Issuer)
|
# -- Certificate issuer kind (ClusterIssuer or Issuer)
|
||||||
kind: ''
|
kind: ""
|
||||||
# -- Certificate issuer group
|
# -- Certificate issuer group
|
||||||
group: cert-manager.io
|
group: cert-manager.io
|
||||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||||
duration: ''
|
duration: ""
|
||||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||||
renewBefore: ''
|
renewBefore: ""
|
||||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||||
commonName: ''
|
commonName: ""
|
||||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||||
emailAddresses: []
|
emailAddresses: []
|
||||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||||
@@ -188,11 +190,13 @@ persistence:
|
|||||||
annotations: {}
|
annotations: {}
|
||||||
# -- Persistent Volume storage class.
|
# -- Persistent Volume storage class.
|
||||||
# If undefined (the default) cluster's default provisioner will be used.
|
# If undefined (the default) cluster's default provisioner will be used.
|
||||||
storageClass: ''
|
storageClass: ""
|
||||||
# -- Persistent Volume Access Mode
|
# -- Persistent Volume Access Mode
|
||||||
accessMode: ReadWriteOnce
|
accessMode: ReadWriteOnce
|
||||||
# -- Persistent Volume size
|
# -- Persistent Volume size
|
||||||
size: 1Gi
|
size: 1Gi
|
||||||
|
# -- Persistent Volume Name (optional)
|
||||||
|
volumeName: ""
|
||||||
|
|
||||||
serviceAccount:
|
serviceAccount:
|
||||||
# -- Specifies whether a service account should be created
|
# -- Specifies whether a service account should be created
|
||||||
@@ -203,7 +207,7 @@ serviceAccount:
|
|||||||
automount: false
|
automount: false
|
||||||
# -- The name of the service account to use.
|
# -- The name of the service account to use.
|
||||||
# If not set and create is true, a name is generated using the fullname template
|
# If not set and create is true, a name is generated using the fullname template
|
||||||
name: ''
|
name: ""
|
||||||
|
|
||||||
monitoring:
|
monitoring:
|
||||||
# -- Enable Prometheus monitoring.
|
# -- Enable Prometheus monitoring.
|
||||||
@@ -220,15 +224,15 @@ monitoring:
|
|||||||
annotations: {}
|
annotations: {}
|
||||||
# -- Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used.
|
# -- Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used.
|
||||||
# @default -- `1m`
|
# @default -- `1m`
|
||||||
interval: ''
|
interval: ""
|
||||||
# -- Relabelings to samples before ingestion.
|
# -- Relabelings to samples before ingestion.
|
||||||
metricRelabelings: []
|
metricRelabelings: []
|
||||||
# -- Relabelings to samples before scraping.
|
# -- Relabelings to samples before scraping.
|
||||||
relabelings: []
|
relabelings: []
|
||||||
# -- Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used.
|
# -- Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used.
|
||||||
scrapeTimeout: ''
|
scrapeTimeout: ""
|
||||||
# -- The label to use to retrieve the job name from.
|
# -- The label to use to retrieve the job name from.
|
||||||
jobLabel: ''
|
jobLabel: ""
|
||||||
# -- Transfers labels on the Kubernetes Pod onto the target.
|
# -- Transfers labels on the Kubernetes Pod onto the target.
|
||||||
podTargetLabels: {}
|
podTargetLabels: {}
|
||||||
|
|
||||||
@@ -241,4 +245,4 @@ monitoring:
|
|||||||
labels: {}
|
labels: {}
|
||||||
# -- Dashboard ConfigMap namespace
|
# -- Dashboard ConfigMap namespace
|
||||||
# Overrides the namespace for the dashboard ConfigMap.
|
# Overrides the namespace for the dashboard ConfigMap.
|
||||||
namespace: ''
|
namespace: ""
|
||||||
|
BIN
docs/assets/images/dashboard.png
Executable file
BIN
docs/assets/images/dashboard.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 269 KiB |
@@ -1,10 +1,12 @@
|
|||||||
Below are some sample YAML configurations demonstrating how to override some default values.
|
Below are some sample YAML configurations demonstrating how to override some default values.
|
||||||
|
|
||||||
## Basic Configuration
|
## Basic
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
core:
|
core:
|
||||||
admin_user: test@example.com
|
admin_user: test@example.com
|
||||||
admin_password: password
|
admin_password: password
|
||||||
|
admin_api_token: super-s3cr3t-api-token-or-a-UUID
|
||||||
import_existing: false
|
import_existing: false
|
||||||
create_default_peer: true
|
create_default_peer: true
|
||||||
self_provisioning_allowed: true
|
self_provisioning_allowed: true
|
||||||
@@ -31,17 +33,16 @@ database:
|
|||||||
dsn: data/sqlite.db
|
dsn: data/sqlite.db
|
||||||
```
|
```
|
||||||
|
|
||||||
## LDAP Authentication and Synchronization Configuration
|
## LDAP Authentication and Synchronization
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ... (basic configuration)
|
# ... (basic configuration)
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
ldap:
|
ldap:
|
||||||
|
|
||||||
# a sample LDAP provider with user sync enabled
|
# a sample LDAP provider with user sync enabled
|
||||||
- id: ldap
|
- id: ldap
|
||||||
provider_name: Active Directory
|
provider_name: Active Directory
|
||||||
display_name: Login with</br>AD
|
|
||||||
url: ldap://srv-ad1.company.local:389
|
url: ldap://srv-ad1.company.local:389
|
||||||
bind_user: ldap_wireguard@company.local
|
bind_user: ldap_wireguard@company.local
|
||||||
bind_pass: super-s3cr3t-ldap
|
bind_pass: super-s3cr3t-ldap
|
||||||
@@ -63,12 +64,24 @@ auth:
|
|||||||
log_user_info: true
|
log_user_info: true
|
||||||
```
|
```
|
||||||
|
|
||||||
## OpenID Connect (OIDC) Authentication Configuration
|
## OpenID Connect (OIDC) Authentication
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ... (basic configuration)
|
# ... (basic configuration)
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
oidc:
|
oidc:
|
||||||
|
# a sample Entra ID provider with environment variable substitution
|
||||||
|
- 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
|
# a sample provider where users with the attribute `wg_admin` set to `true` are considered as admins
|
||||||
- id: oidc-with-admin-attribute
|
- id: oidc-with-admin-attribute
|
||||||
@@ -89,7 +102,7 @@ auth:
|
|||||||
department: department
|
department: department
|
||||||
is_admin: wg_admin
|
is_admin: wg_admin
|
||||||
admin_mapping:
|
admin_mapping:
|
||||||
- admin_value_regex: ^true$
|
admin_value_regex: ^true$
|
||||||
registration_enabled: true
|
registration_enabled: true
|
||||||
log_user_info: true
|
log_user_info: true
|
||||||
|
|
||||||
@@ -112,18 +125,18 @@ auth:
|
|||||||
department: department
|
department: department
|
||||||
user_groups: groups
|
user_groups: groups
|
||||||
admin_mapping:
|
admin_mapping:
|
||||||
- admin_group_regex: ^the-admin-group$
|
admin_group_regex: ^the-admin-group$
|
||||||
registration_enabled: true
|
registration_enabled: true
|
||||||
log_user_info: true
|
log_user_info: true
|
||||||
```
|
```
|
||||||
|
|
||||||
## Plain OAuth2 Authentication Configuration
|
## Plain OAuth2 Authentication
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ... (basic configuration)
|
# ... (basic configuration)
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
oauth:
|
oauth:
|
||||||
|
|
||||||
# a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True`
|
# a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True`
|
||||||
# are considered as admins
|
# are considered as admins
|
||||||
- id: google_plain_oauth-with-admin-attribute
|
- id: google_plain_oauth-with-admin-attribute
|
||||||
@@ -144,7 +157,7 @@ auth:
|
|||||||
firstname: name
|
firstname: name
|
||||||
is_admin: this-attribute-must-be-true
|
is_admin: this-attribute-must-be-true
|
||||||
admin_mapping:
|
admin_mapping:
|
||||||
- admin_value_regex: ^(True|true)$
|
admin_value_regex: ^(True|true)$
|
||||||
registration_enabled: true
|
registration_enabled: true
|
||||||
|
|
||||||
# a sample provider where either users with the attribute `this-attribute-must-be-true` set to `true` or
|
# a sample provider where either users with the attribute `this-attribute-must-be-true` set to `true` or
|
||||||
|
@@ -1,10 +1,105 @@
|
|||||||
# WireGuard Portal Configuration
|
|
||||||
|
|
||||||
This page provides an overview of **all available configuration options** for WireGuard Portal.
|
This page provides an overview of **all available configuration options** for WireGuard Portal.
|
||||||
You can supply these configurations in a **YAML** file (e.g. `config.yaml`) when starting the Portal.
|
|
||||||
Complete configuration examples are available in the [Configuration Examples](./examples.md) page.
|
|
||||||
|
|
||||||
Below you will find sections like `core`, `advanced`, `statistics`, `mail`, `auth`, `database`, and `web`.
|
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: true
|
||||||
|
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: ""
|
||||||
|
|
||||||
|
webhook:
|
||||||
|
url: ""
|
||||||
|
authentication: ""
|
||||||
|
timeout: 10s
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
Below you will find sections like
|
||||||
|
[`core`](#core),
|
||||||
|
[`advanced`](#advanced),
|
||||||
|
[`database`](#database),
|
||||||
|
[`statistics`](#statistics),
|
||||||
|
[`mail`](#mail),
|
||||||
|
[`auth`](#auth),
|
||||||
|
[`web`](#web) and
|
||||||
|
[`webhook`](#webhook).
|
||||||
Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose.
|
Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -22,6 +117,10 @@ More advanced options are found in the subsequent `Advanced` section.
|
|||||||
- **Default:** `wgportal`
|
- **Default:** `wgportal`
|
||||||
- **Description:** The administrator password. The default password of `wgportal` should be changed immediately.
|
- **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`
|
### `editable_keys`
|
||||||
- **Default:** `true`
|
- **Default:** `true`
|
||||||
- **Description:** Allow editing of WireGuard key-pairs directly in the UI.
|
- **Description:** Allow editing of WireGuard key-pairs directly in the UI.
|
||||||
@@ -108,7 +207,6 @@ Additional or more specialized configuration options for logging and interface c
|
|||||||
- **Default:** `true`
|
- **Default:** `true`
|
||||||
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
|
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
@@ -197,7 +295,7 @@ Options for configuring email notifications or sending peer configurations via e
|
|||||||
- **Description:** SMTP encryption type. Valid values: `none`, `tls`, `starttls`.
|
- **Description:** SMTP encryption type. Valid values: `none`, `tls`, `starttls`.
|
||||||
|
|
||||||
### `cert_validation`
|
### `cert_validation`
|
||||||
- **Default:** `false`
|
- **Default:** `true`
|
||||||
- **Description:** If `true`, validate the SMTP server certificate (relevant if `encryption` = `tls`).
|
- **Description:** If `true`, validate the SMTP server certificate (relevant if `encryption` = `tls`).
|
||||||
|
|
||||||
### `username`
|
### `username`
|
||||||
@@ -229,7 +327,7 @@ Each can have multiple providers configured. Below are the relevant keys.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### OIDC Provider Properties
|
### OIDC
|
||||||
|
|
||||||
The `oidc` array contains a list of OpenID Connect providers.
|
The `oidc` array contains a list of OpenID Connect providers.
|
||||||
Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
||||||
@@ -290,7 +388,7 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### OAuth Provider Properties
|
### OAuth
|
||||||
|
|
||||||
The `oauth` array contains a list of plain OAuth2 providers.
|
The `oauth` array contains a list of plain OAuth2 providers.
|
||||||
Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
||||||
@@ -359,11 +457,15 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### LDAP Provider Properties
|
### LDAP
|
||||||
|
|
||||||
The `ldap` array contains a list of LDAP authentication providers.
|
The `ldap` array contains a list of LDAP authentication providers.
|
||||||
Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
||||||
|
|
||||||
|
#### `provider_name`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||||
|
|
||||||
#### `url`
|
#### `url`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** The LDAP server URL (e.g., `ldap://srv-ad01.company.local:389`).
|
- **Description:** The LDAP server URL (e.g., `ldap://srv-ad01.company.local:389`).
|
||||||
@@ -455,3 +557,82 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
|||||||
#### `log_user_info`
|
#### `log_user_info`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** If `true`, logs LDAP user data at the trace level upon login.
|
- **Description:** If `true`, logs LDAP user data at the trace level upon login.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Web
|
||||||
|
|
||||||
|
The web section contains configuration options for the web server, including the listening address, session management, and CSRF protection.
|
||||||
|
It is important to specify a valid `external_url` for the web server, especially if you are using a reverse proxy.
|
||||||
|
Without a valid `external_url`, the login process may fail due to CSRF protection.
|
||||||
|
|
||||||
|
### `listening_address`
|
||||||
|
- **Default:** `:8888`
|
||||||
|
- **Description:** The listening port of the web server.
|
||||||
|
|
||||||
|
### `external_url`
|
||||||
|
- **Default:** `http://localhost:8888`
|
||||||
|
- **Description:** The URL where a client can access WireGuard Portal. This URL is used for generating links in emails and for performing OAUTH redirects.
|
||||||
|
**Important:** If you are using a reverse proxy, set this to the external URL of the reverse proxy, otherwise login will fail. If you access the portal via IP address, set this to the IP address of the server.
|
||||||
|
|
||||||
|
### `site_company_name`
|
||||||
|
- **Default:** `WireGuard Portal`
|
||||||
|
- **Description:** The company name that is shown at the bottom of the web frontend.
|
||||||
|
|
||||||
|
### `site_title`
|
||||||
|
- **Default:** `WireGuard Portal`
|
||||||
|
- **Description:** The title that is shown in the web frontend.
|
||||||
|
|
||||||
|
### `session_identifier`
|
||||||
|
- **Default:** `wgPortalSession`
|
||||||
|
- **Description:** The session identifier for the web frontend.
|
||||||
|
|
||||||
|
### `session_secret`
|
||||||
|
- **Default:** `very_secret`
|
||||||
|
- **Description:** The session secret for the web frontend.
|
||||||
|
|
||||||
|
### `csrf_secret`
|
||||||
|
- **Default:** `extremely_secret`
|
||||||
|
- **Description:** The CSRF secret.
|
||||||
|
|
||||||
|
### `request_logging`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** Log all HTTP requests.
|
||||||
|
|
||||||
|
### `cert_file`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** (Optional) Path to the TLS certificate file.
|
||||||
|
|
||||||
|
### `key_file`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** (Optional) Path to the TLS certificate key file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Webhook
|
||||||
|
|
||||||
|
The webhook section allows you to configure a webhook that is called on certain events in WireGuard Portal.
|
||||||
|
A JSON object is sent in a POST request to the webhook URL with the following structure:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "peer_created",
|
||||||
|
"entity": "peer",
|
||||||
|
"identifier": "the-peer-identifier",
|
||||||
|
"payload": {
|
||||||
|
// The payload of the event, e.g. peer data.
|
||||||
|
// Check the API documentation for the exact structure.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `url`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The POST endpoint to which the webhook is sent. The URL must be reachable from the WireGuard Portal server. If the URL is empty, the webhook is disabled.
|
||||||
|
|
||||||
|
### `authentication`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The Authorization header for the webhook endpoint. The value is send as-is in the header. For example: `Bearer <token>`.
|
||||||
|
|
||||||
|
### `timeout`
|
||||||
|
- **Default:** `10s`
|
||||||
|
- **Description:** The timeout for the webhook request. If the request takes longer than this, it is aborted.
|
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.23** or higher has to be installed to build WireGuard Portal.
|
|
||||||
If you want to re-compile the frontend, NodeJS **18** and NPM >= **9** is required.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# build the frontend (optional)
|
|
||||||
make frontend
|
|
||||||
|
|
||||||
# build the binary
|
|
||||||
make build
|
|
||||||
```
|
|
@@ -5,20 +5,7 @@ The preferred way to start WireGuard Portal as Docker container is to use Docker
|
|||||||
A sample docker-compose.yml:
|
A sample docker-compose.yml:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3.6'
|
--8<-- "docker-compose.yml::17"
|
||||||
services:
|
|
||||||
wg-portal:
|
|
||||||
image: wgportal/wg-portal:latest
|
|
||||||
restart: unless-stopped
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
network_mode: "host"
|
|
||||||
ports:
|
|
||||||
- "8888:8888"
|
|
||||||
volumes:
|
|
||||||
- /etc/wireguard:/etc/wireguard
|
|
||||||
- ./data:/app/data
|
|
||||||
- ./config:/app/config
|
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, the webserver is listening on port **8888**.
|
By default, the webserver is listening on port **8888**.
|
||||||
@@ -31,6 +18,7 @@ All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-por
|
|||||||
There are three types of tags in the repository:
|
There are three types of tags in the repository:
|
||||||
|
|
||||||
#### Semantic versioned tags
|
#### Semantic versioned tags
|
||||||
|
|
||||||
For example, `1.0.19`.
|
For example, `1.0.19`.
|
||||||
|
|
||||||
These are official releases of WireGuard Portal. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases).
|
These are official releases of WireGuard Portal. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases).
|
||||||
@@ -44,16 +32,17 @@ If you only want to stay at the same major or major+minor version, use either `v
|
|||||||
Version **1** is currently **stable**, version **2** is in **development**.
|
Version **1** is currently **stable**, version **2** is in **development**.
|
||||||
|
|
||||||
#### latest
|
#### latest
|
||||||
|
|
||||||
This is the most recent build to master! It changes a lot and is very unstable.
|
This is the most recent build to master! It changes a lot and is very unstable.
|
||||||
|
|
||||||
We recommend that you don't use it except for development purposes.
|
We recommend that you don't use it except for development purposes.
|
||||||
|
|
||||||
#### Branch tags
|
#### Branch tags
|
||||||
|
|
||||||
For each commit in the master and the stable branch, a corresponding Docker image is build. These images use the `master` or `stable` tags.
|
For each commit in the master and the stable branch, a corresponding Docker image is build. These images use the `master` or `stable` tags.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
You can configure WireGuard Portal using a yaml configuration file.
|
You can configure WireGuard Portal using a yaml configuration file.
|
||||||
The filepath of the yaml configuration file defaults to `/app/config/config.yml`.
|
The filepath of the yaml configuration file defaults to `/app/config/config.yml`.
|
||||||
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
|
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
|
||||||
@@ -61,6 +50,7 @@ It is possible to override the configuration filepath using the environment vari
|
|||||||
By default, WireGuard Portal uses a SQLite database. The database is stored in `/app/data/sqlite.db`.
|
By default, WireGuard Portal uses a SQLite database. The database is stored in `/app/data/sqlite.db`.
|
||||||
|
|
||||||
You should mount those directories as a volume:
|
You should mount those directories as a volume:
|
||||||
|
|
||||||
- /app/data
|
- /app/data
|
||||||
- /app/config
|
- /app/config
|
||||||
|
|
||||||
|
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.24.0`
|
||||||
|
- [Node.js with npm](https://nodejs.org/en/download): `node>=18, npm>=9`
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Get source code
|
||||||
|
git clone https://github.com/h44z/wg-portal -b ${WG_PORTAL_VERSION:-master} --depth 1
|
||||||
|
cd wg-portal
|
||||||
|
# Build the frontend
|
||||||
|
make frontend
|
||||||
|
# Build the backend
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Compiled binary will be available in `./dist` directory.
|
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).
|
--8<-- "README.md:20:47"
|
||||||
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
|
|
||||||
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
|
|
||||||
connections.
|
|
||||||
|
|
||||||
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP
|
|
||||||
(Active Directory or OpenLDAP) as a user source for authentication and profile data.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
* Self-hosted - the whole application is a single binary
|
|
||||||
* Responsive web UI written in Vue.JS
|
|
||||||
* Automatically select IP from the network pool assigned to client
|
|
||||||
* QR-Code for convenient mobile client configuration
|
|
||||||
* Sent email to client with QR-code and client config
|
|
||||||
* Enable / Disable clients seamlessly
|
|
||||||
* Generation of wg-quick configuration file (`wgX.conf`) if required
|
|
||||||
* User authentication (database, OAuth or LDAP)
|
|
||||||
* IPv6 ready
|
|
||||||
* Docker ready
|
|
||||||
* Can be used with existing WireGuard setups
|
|
||||||
* Support for multiple WireGuard interfaces
|
|
||||||
* Peer Expiry Feature
|
|
||||||
* Handle route and DNS settings like wg-quick does
|
|
||||||
* REST API for management and client deployment
|
|
||||||
|
|
||||||
## Quick-Start
|
|
||||||
|
|
||||||
The easiest way to get started is to use the provided [Docker image](./getting-started/docker.md).
|
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ You can also specify the database type using the parameter **-migrateFromType**,
|
|||||||
For example:
|
For example:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom=user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom='user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local'
|
||||||
```
|
```
|
||||||
|
|
||||||
The upgrade will transform the old, existing database and store the values in the new database specified in the **config.yml** configuration file.
|
The upgrade will transform the old, existing database and store the values in the new database specified in the **config.yml** configuration file.
|
1399
frontend/package-lock.json
generated
1399
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,25 +8,27 @@
|
|||||||
"preview": "vite preview --port 5050"
|
"preview": "vite preview --port 5050"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fontsource/nunito-sans": "^5.2.5",
|
||||||
"@kyvg/vue3-notification": "^3.1.3",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
|
"@kyvg/vue3-notification": "^3.4.1",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.2",
|
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
||||||
"bootswatch": "^5.3.2",
|
"bootstrap": "^5.3.5",
|
||||||
"flag-icons": "^7.1.0",
|
"bootswatch": "^5.3.5",
|
||||||
"ip-address": "^9.0.5",
|
"flag-icons": "^7.3.2",
|
||||||
"is-cidr": "^5.0.3",
|
"ip-address": "^10.0.1",
|
||||||
|
"is-cidr": "^5.1.1",
|
||||||
"is-ip": "^5.0.1",
|
"is-ip": "^5.0.1",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^3.0.2",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.30.0",
|
||||||
"vue": "^3.3.13",
|
"vue": "^3.5.13",
|
||||||
"vue-i18n": "^9.14.2",
|
"vue-i18n": "^11.1.3",
|
||||||
"vue-prism-component": "github:h44z/vue-prism-component",
|
"vue-prism-component": "github:h44z/vue-prism-component",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.5.0"
|
||||||
"vue3-tags-input": "^1.0.12"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.5.2",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"vite": "^5.0.10"
|
"sass-embedded": "^1.86.3",
|
||||||
|
"vite": "6.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import { computed, getCurrentInstance, onMounted, ref } from "vue";
|
|||||||
import { authStore } from "./stores/auth";
|
import { authStore } from "./stores/auth";
|
||||||
import { securityStore } from "./stores/security";
|
import { securityStore } from "./stores/security";
|
||||||
import { settingsStore } from "@/stores/settings";
|
import { settingsStore } from "@/stores/settings";
|
||||||
|
import { Notifications } from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
const appGlobal = getCurrentInstance().appContext.config.globalProperties
|
const appGlobal = getCurrentInstance().appContext.config.globalProperties
|
||||||
const auth = authStore()
|
const auth = authStore()
|
||||||
@@ -45,13 +46,12 @@ const languageFlag = computed(() => {
|
|||||||
if (!appGlobal.$i18n.availableLocales.includes(lang)) {
|
if (!appGlobal.$i18n.availableLocales.includes(lang)) {
|
||||||
lang = appGlobal.$i18n.fallbackLocale;
|
lang = appGlobal.$i18n.fallbackLocale;
|
||||||
}
|
}
|
||||||
if (lang === "en") {
|
const langMap = {
|
||||||
lang = "us";
|
en: "us",
|
||||||
}
|
uk: "ua",
|
||||||
if (lang === "zh") {
|
zh: "cn",
|
||||||
lang = "cn";
|
};
|
||||||
}
|
return "fi-" + (langMap[lang] || lang);
|
||||||
return "fi-" + lang;
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
||||||
@@ -91,6 +91,7 @@ const currentYear = ref(new Date().getFullYear())
|
|||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
||||||
<RouterLink :to="{ name: 'settings' }" class="dropdown-item" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</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>
|
||||||
|
<RouterLink :to="{ name: 'audit' }" class="dropdown-item" v-if="auth.IsAdmin"><i class="fas fa-file-shield"></i> {{ $t('menu.audit') }}</RouterLink>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="auth.Logout"><i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}</a>
|
<a class="dropdown-item" href="#" @click.prevent="auth.Logout"><i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,9 +118,11 @@ const currentYear = ref(new Date().getFullYear())
|
|||||||
<button aria-expanded="false" aria-haspopup="true" class="btn btn btn-secondary pe-0"
|
<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>
|
data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button>
|
||||||
<div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style="">
|
<div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style="">
|
||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
|
|
||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
|
||||||
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
|
||||||
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('fr')"><span class="fi fi-fr"></span> Français</a>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ru')"><span class="fi fi-ru"></span> Русский</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('vi')"><span class="fi fi-vi"></span> Tiếng Việt</a>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('zh')"><span class="fi fi-cn"></span> 中文</a>
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('zh')"><span class="fi fi-cn"></span> 中文</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -15,3 +15,85 @@ a.disabled {
|
|||||||
.desc::after {
|
.desc::after {
|
||||||
content: " ↓";
|
content: " ↓";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* style the background and the text color of the input ... */
|
||||||
|
.vue-tags-input {
|
||||||
|
max-width: 100% !important;
|
||||||
|
background-color: #f7f7f9 !important;
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input .ti-input {
|
||||||
|
padding: 0 0;
|
||||||
|
border: none !important;
|
||||||
|
transition: border-bottom 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input .ti-new-tag-input {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
padding: 0.75rem 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* style the placeholders color across all browser */
|
||||||
|
.vue-tags-input ::-webkit-input-placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
.vue-tags-input .ti-input::placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input ::-moz-placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input :-ms-input-placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input :-moz-placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* default styles for all the tags */
|
||||||
|
.vue-tags-input .ti-tag {
|
||||||
|
position: relative;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2px solid var(--bs-body-color);
|
||||||
|
margin: 6px;
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the styles if a tag is invalid */
|
||||||
|
.vue-tags-input .ti-tag.ti-invalid {
|
||||||
|
background-color: #e88a74;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if the user input is invalid, the input color should be red */
|
||||||
|
.vue-tags-input .ti-new-tag-input.ti-invalid {
|
||||||
|
color: #e88a74;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if a tag or the user input is a duplicate, it should be crossed out */
|
||||||
|
.vue-tags-input .ti-duplicate span,
|
||||||
|
.vue-tags-input .ti-new-tag-input.ti-duplicate {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if the user presses backspace, the complete tag should be crossed out, to mark it for deletion */
|
||||||
|
.vue-tags-input .ti-tag:after {
|
||||||
|
transition: transform .2s;
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
height: 2px;
|
||||||
|
width: 108%;
|
||||||
|
left: -4%;
|
||||||
|
top: calc(50% - 1px);
|
||||||
|
background-color: #000;
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input .ti-deletion-mark:after {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
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;
|
||||||
|
*/
|
@@ -4,7 +4,7 @@ import {interfaceStore} from "@/stores/interfaces";
|
|||||||
import {computed, ref, watch} from "vue";
|
import {computed, ref, watch} from "vue";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import Vue3TagsInput from 'vue3-tags-input';
|
import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
|
||||||
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
||||||
import isCidr from "is-cidr";
|
import isCidr from "is-cidr";
|
||||||
import {isIP} from 'is-ip';
|
import {isIP} from 'is-ip';
|
||||||
@@ -38,6 +38,15 @@ const title = computed(() => {
|
|||||||
return t("modals.interface-edit.headline-new")
|
return t("modals.interface-edit.headline-new")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const currentTags = ref({
|
||||||
|
Addresses: "",
|
||||||
|
Dns: "",
|
||||||
|
DnsSearch: "",
|
||||||
|
PeerDefNetwork: "",
|
||||||
|
PeerDefAllowedIPs: "",
|
||||||
|
PeerDefDns: "",
|
||||||
|
PeerDefDnsSearch: ""
|
||||||
|
})
|
||||||
const formData = ref(freshInterface())
|
const formData = ref(freshInterface())
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
@@ -137,94 +146,94 @@ function close() {
|
|||||||
function handleChangeAddresses(tags) {
|
function handleChangeAddresses(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if(isCidr(tag) === 0) {
|
if(isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(validInput) {
|
if(validInput) {
|
||||||
formData.value.Addresses = tags
|
formData.value.Addresses = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeDns(tags) {
|
function handleChangeDns(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if(!isIP(tag)) {
|
if(!isIP(tag.text)) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid IP",
|
title: "Invalid IP",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(validInput) {
|
if(validInput) {
|
||||||
formData.value.Dns = tags
|
formData.value.Dns = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeDnsSearch(tags) {
|
function handleChangeDnsSearch(tags) {
|
||||||
formData.value.DnsSearch = tags
|
formData.value.DnsSearch = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangePeerDefNetwork(tags) {
|
function handleChangePeerDefNetwork(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if(isCidr(tag) === 0) {
|
if(isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(validInput) {
|
if(validInput) {
|
||||||
formData.value.PeerDefNetwork = tags
|
formData.value.PeerDefNetwork = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangePeerDefAllowedIPs(tags) {
|
function handleChangePeerDefAllowedIPs(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if(isCidr(tag) === 0) {
|
if(isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(validInput) {
|
if(validInput) {
|
||||||
formData.value.PeerDefAllowedIPs = tags
|
formData.value.PeerDefAllowedIPs = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangePeerDefDns(tags) {
|
function handleChangePeerDefDns(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if(!isIP(tag)) {
|
if(!isIP(tag.text)) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid IP",
|
title: "Invalid IP",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(validInput) {
|
if(validInput) {
|
||||||
formData.value.PeerDefDns = tags
|
formData.value.PeerDefDns = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangePeerDefDnsSearch(tags) {
|
function handleChangePeerDefDnsSearch(tags) {
|
||||||
formData.value.PeerDefDnsSearch = tags
|
formData.value.PeerDefDnsSearch = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
@@ -333,11 +342,15 @@ async function del() {
|
|||||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
|
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.Addresses"
|
<vue-tags-input class="form-control" v-model="currentTags.Addresses"
|
||||||
:placeholder="$t('modals.interface-edit.ip.placeholder')"
|
:tags="formData.Addresses.map(str => ({ text: str }))"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:placeholder="$t('modals.interface-edit.ip.placeholder')"
|
||||||
:validate="validateCIDR"
|
:validation="validateCIDR()"
|
||||||
@on-tags-changed="handleChangeAddresses"/>
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeAddresses"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="formData.Mode==='server'" class="form-group">
|
<div v-if="formData.Mode==='server'" class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label>
|
||||||
@@ -345,19 +358,27 @@ async function del() {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="formData.Mode!=='server'" class="form-group">
|
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.Dns"
|
<vue-tags-input class="form-control" v-model="currentTags.Dns"
|
||||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
:tags="formData.Dns.map(str => ({ text: str }))"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||||
:validate="validateIP"
|
:validation="validateIP()"
|
||||||
@on-tags-changed="handleChangeDns"/>
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeDns"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="formData.Mode!=='server'" class="form-group">
|
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch"
|
<vue-tags-input class="form-control" v-model="currentTags.DnsSearch"
|
||||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
:tags="formData.DnsSearch.map(str => ({ text: str }))"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||||
:validate="validateDomain"
|
:validation="validateDomain()"
|
||||||
@on-tags-changed="handleChangeDnsSearch"/>
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeDnsSearch"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
@@ -420,36 +441,52 @@ async function del() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefNetwork"
|
<vue-tags-input class="form-control" v-model="currentTags.PeerDefNetwork"
|
||||||
:placeholder="$t('modals.interface-edit.defaults.networks.placeholder')"
|
:tags="formData.PeerDefNetwork.map(str => ({ text: str }))"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:placeholder="$t('modals.interface-edit.defaults.networks.placeholder')"
|
||||||
:validate="validateCIDR"
|
:validation="validateCIDR()"
|
||||||
@on-tags-changed="handleChangePeerDefNetwork"/>
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangePeerDefNetwork"/>
|
||||||
<small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.networks.description') }}</small>
|
<small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.networks.description') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowed-ip.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowed-ip.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefAllowedIPs"
|
<vue-tags-input class="form-control" v-model="currentTags.PeerDefAllowedIPs"
|
||||||
:placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')"
|
:tags="formData.PeerDefAllowedIPs.map(str => ({ text: str }))"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')"
|
||||||
:validate="validateCIDR"
|
:validation="validateCIDR()"
|
||||||
@on-tags-changed="handleChangePeerDefAllowedIPs"/>
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangePeerDefAllowedIPs"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefDns"
|
<vue-tags-input class="form-control" v-model="currentTags.PeerDefDns"
|
||||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
:tags="formData.PeerDefDns.map(str => ({ text: str }))"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||||
:validate="validateIP"
|
:validation="validateIP()"
|
||||||
@on-tags-changed="handleChangePeerDefDns"/>
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangePeerDefDns"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefDnsSearch"
|
<vue-tags-input class="form-control" v-model="currentTags.PeerDefDnsSearch"
|
||||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
:tags="formData.PeerDefDnsSearch.map(str => ({ text: str }))"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||||
:validate="validateDomain"
|
:validation="validateDomain()"
|
||||||
@on-tags-changed="handleChangePeerDefDnsSearch"/>
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangePeerDefDnsSearch"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
|
@@ -5,7 +5,7 @@ import { interfaceStore } from "@/stores/interfaces";
|
|||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import Vue3TagsInput from "vue3-tags-input";
|
import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
|
||||||
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
||||||
import isCidr from "is-cidr";
|
import isCidr from "is-cidr";
|
||||||
import { isIP } from 'is-ip';
|
import { isIP } from 'is-ip';
|
||||||
@@ -65,6 +65,13 @@ const title = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const currentTags = ref({
|
||||||
|
Addresses: "",
|
||||||
|
AllowedIPs: "",
|
||||||
|
ExtraAllowedIPs: "",
|
||||||
|
Dns: "",
|
||||||
|
DnsSearch: ""
|
||||||
|
})
|
||||||
const formData = ref(freshPeer())
|
const formData = ref(freshPeer())
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
@@ -193,73 +200,73 @@ function close() {
|
|||||||
function handleChangeAddresses(tags) {
|
function handleChangeAddresses(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if (isCidr(tag) === 0) {
|
if (isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (validInput) {
|
if (validInput) {
|
||||||
formData.value.Addresses = tags
|
formData.value.Addresses = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeAllowedIPs(tags) {
|
function handleChangeAllowedIPs(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if (isCidr(tag) === 0) {
|
if (isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (validInput) {
|
if (validInput) {
|
||||||
formData.value.AllowedIPs.Value = tags
|
formData.value.AllowedIPs.Value = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeExtraAllowedIPs(tags) {
|
function handleChangeExtraAllowedIPs(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if (isCidr(tag) === 0) {
|
if (isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (validInput) {
|
if (validInput) {
|
||||||
formData.value.ExtraAllowedIPs = tags
|
formData.value.ExtraAllowedIPs = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeDns(tags) {
|
function handleChangeDns(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if (!isIP(tag)) {
|
if (!isIP(tag.text)) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid IP",
|
title: "Invalid IP",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (validInput) {
|
if (validInput) {
|
||||||
formData.value.Dns.Value = tags
|
formData.value.Dns.Value = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeDnsSearch(tags) {
|
function handleChangeDnsSearch(tags) {
|
||||||
formData.value.DnsSearch.Value = tags
|
formData.value.DnsSearch.Value = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
@@ -344,34 +351,64 @@ async function del() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.Addresses"
|
<vue-tags-input class="form-control" v-model="currentTags.Addresses"
|
||||||
:placeholder="$t('modals.peer-edit.ip.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]"
|
:tags="formData.Addresses.map(str => ({ text: str }))"
|
||||||
:validate="validateCIDR" @on-tags-changed="handleChangeAddresses" />
|
:placeholder="$t('modals.peer-edit.ip.placeholder')"
|
||||||
|
:validation="validateCIDR()"
|
||||||
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeAddresses" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.allowed-ip.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.allowed-ip.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.AllowedIPs.Value"
|
<vue-tags-input class="form-control" v-model="currentTags.AllowedIPs"
|
||||||
:placeholder="$t('modals.peer-edit.allowed-ip.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]"
|
:tags="formData.AllowedIPs.Value.map(str => ({ text: str }))"
|
||||||
:validate="validateCIDR" @on-tags-changed="handleChangeAllowedIPs" />
|
:placeholder="$t('modals.peer-edit.allowed-ip.placeholder')"
|
||||||
|
:validation="validateCIDR()"
|
||||||
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeAllowedIPs" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.extra-allowed-ip.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.extra-allowed-ip.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.ExtraAllowedIPs"
|
<vue-tags-input class="form-control" v-model="currentTags.ExtraAllowedIPs"
|
||||||
:placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]"
|
:tags="formData.ExtraAllowedIPs.map(str => ({ text: str }))"
|
||||||
:validate="validateCIDR" @on-tags-changed="handleChangeExtraAllowedIPs" />
|
:placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')"
|
||||||
|
:validation="validateCIDR()"
|
||||||
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeExtraAllowedIPs" />
|
||||||
<small class="form-text text-muted">{{ $t('modals.peer-edit.extra-allowed-ip.description') }}</small>
|
<small class="form-text text-muted">{{ $t('modals.peer-edit.extra-allowed-ip.description') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.Dns.Value"
|
<vue-tags-input class="form-control" v-model="currentTags.Dns"
|
||||||
:placeholder="$t('modals.peer-edit.dns.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]"
|
:tags="formData.Dns.Value.map(str => ({ text: str }))"
|
||||||
:validate="validateIP" @on-tags-changed="handleChangeDns" />
|
:placeholder="$t('modals.peer-edit.dns.placeholder')"
|
||||||
|
:validation="validateIP()"
|
||||||
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeDns" />
|
||||||
</div>
|
</div>
|
||||||
<div hidden class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns-search.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns-search.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch.Value"
|
<vue-tags-input class="form-control" v-model="currentTags.DnsSearch"
|
||||||
:placeholder="$t('modals.peer-edit.dns-search.label')" :add-tag-on-keys="[13, 188, 32, 9]"
|
:tags="formData.DnsSearch.Value.map(str => ({ text: str }))"
|
||||||
:validate="validateDomain" @on-tags-changed="handleChangeDnsSearch" />
|
:placeholder="$t('modals.peer-edit.dns-search.label')"
|
||||||
|
:validation="validateDomain()"
|
||||||
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeDnsSearch" />
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
|
@@ -5,7 +5,7 @@ import {interfaceStore} from "@/stores/interfaces";
|
|||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import Vue3TagsInput from "vue3-tags-input";
|
import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
|
||||||
import { freshInterface } from '@/helpers/models';
|
import { freshInterface } from '@/helpers/models';
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -36,6 +36,7 @@ function freshForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentTag = ref("")
|
||||||
const formData = ref(freshForm())
|
const formData = ref(freshForm())
|
||||||
|
|
||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
@@ -55,7 +56,7 @@ function close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeUserIdentifiers(tags) {
|
function handleChangeUserIdentifiers(tags) {
|
||||||
formData.value.Identifiers = tags
|
formData.value.Identifiers = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
@@ -89,10 +90,14 @@ async function save() {
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.identifiers.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.identifiers.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.Identifiers"
|
<vue-tags-input class="form-control" v-model="currentTag"
|
||||||
:placeholder="$t('modals.peer-multi-create.identifiers.placeholder')"
|
:tags="formData.Identifiers.map(str => ({ text: str }))"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:placeholder="$t('modals.peer-multi-create.identifiers.placeholder')"
|
||||||
@on-tags-changed="handleChangeUserIdentifiers"/>
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeUserIdentifiers"/>
|
||||||
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.identifiers.description') }}</small>
|
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.identifiers.description') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@@ -89,19 +89,11 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
|||||||
|
|
||||||
function download() {
|
function download() {
|
||||||
// credit: https://www.bitdegree.org/learn/javascript-download
|
// credit: https://www.bitdegree.org/learn/javascript-download
|
||||||
let filename = 'WireGuard-Tunnel.conf'
|
|
||||||
if (selectedPeer.value.DisplayName) {
|
|
||||||
filename = selectedPeer.value.DisplayName
|
|
||||||
.replace(/ /g, "_")
|
|
||||||
.replace(/[^a-zA-Z0-9-_]/g, "")
|
|
||||||
.substring(0, 16)
|
|
||||||
+ ".conf"
|
|
||||||
}
|
|
||||||
let text = configString.value
|
let text = configString.value
|
||||||
|
|
||||||
let element = document.createElement('a')
|
let element = document.createElement('a')
|
||||||
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
|
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
|
||||||
element.setAttribute('download', filename)
|
element.setAttribute('download', selectedPeer.value.Filename)
|
||||||
|
|
||||||
element.style.display = 'none'
|
element.style.display = 'none'
|
||||||
document.body.appendChild(element)
|
document.body.appendChild(element)
|
||||||
|
@@ -51,6 +51,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
|||||||
formData.value.Notes = selectedUser.value.Notes
|
formData.value.Notes = selectedUser.value.Notes
|
||||||
formData.value.Password = ""
|
formData.value.Password = ""
|
||||||
formData.value.Disabled = selectedUser.value.Disabled
|
formData.value.Disabled = selectedUser.value.Disabled
|
||||||
|
formData.value.Locked = selectedUser.value.Locked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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>
|
@@ -42,7 +42,8 @@ export function freshInterface() {
|
|||||||
PeerDefPostDown: "",
|
PeerDefPostDown: "",
|
||||||
|
|
||||||
TotalPeers: 0,
|
TotalPeers: 0,
|
||||||
EnabledPeers: 0
|
EnabledPeers: 0,
|
||||||
|
Filename: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +121,11 @@ export function freshPeer() {
|
|||||||
Overridable: true,
|
Overridable: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Internal value
|
Filename: "",
|
||||||
IgnoreGlobalSettings: false
|
|
||||||
|
// Internal values
|
||||||
|
IgnoreGlobalSettings: false,
|
||||||
|
IsSelected: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +152,10 @@ export function freshUser() {
|
|||||||
|
|
||||||
ApiEnabled: false,
|
ApiEnabled: false,
|
||||||
|
|
||||||
PeerCount: 0
|
PeerCount: 0,
|
||||||
|
|
||||||
|
// Internal values
|
||||||
|
IsSelected: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,14 +1,26 @@
|
|||||||
import isCidr from "is-cidr";
|
import isCidr from "is-cidr";
|
||||||
import {isIP} from 'is-ip';
|
import {isIP} from 'is-ip';
|
||||||
|
|
||||||
export function validateCIDR(value) {
|
export function validateCIDR() {
|
||||||
return isCidr(value) !== 0
|
return [{
|
||||||
|
classes: 'invalid-cidr',
|
||||||
|
rule: ({ text }) => isCidr(text) === 0,
|
||||||
|
disableAdd: true,
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateIP(value) {
|
export function validateIP() {
|
||||||
return isIP(value)
|
return [{
|
||||||
|
classes: 'invalid-ip',
|
||||||
|
rule: ({ text }) => !isIP(text),
|
||||||
|
disableAdd: true,
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateDomain(value) {
|
export function validateDomain() {
|
||||||
return true
|
return [{
|
||||||
|
classes: 'invalid-domain',
|
||||||
|
rule: tag => tag.text.length < 3,
|
||||||
|
disableAdd: true,
|
||||||
|
}]
|
||||||
}
|
}
|
@@ -1,7 +1,9 @@
|
|||||||
// src/lang/index.js
|
// src/lang/index.js
|
||||||
import de from './translations/de.json';
|
import de from './translations/de.json';
|
||||||
import ru from './translations/ru.json';
|
|
||||||
import en from './translations/en.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 vi from './translations/vi.json';
|
||||||
import zh from './translations/zh.json';
|
import zh from './translations/zh.json';
|
||||||
import {createI18n} from "vue-i18n";
|
import {createI18n} from "vue-i18n";
|
||||||
@@ -19,10 +21,12 @@ const i18n = createI18n({
|
|||||||
fallbackLocale: "en", // set fallback locale
|
fallbackLocale: "en", // set fallback locale
|
||||||
messages: {
|
messages: {
|
||||||
"de": de,
|
"de": de,
|
||||||
"ru": ru,
|
|
||||||
"en": en,
|
"en": en,
|
||||||
|
"fr": fr,
|
||||||
|
"ru": ru,
|
||||||
|
"uk": uk,
|
||||||
"vi": vi,
|
"vi": vi,
|
||||||
"zh": zh
|
"zh": zh,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -354,7 +354,7 @@
|
|||||||
"endpoint": {
|
"endpoint": {
|
||||||
"label": "Endpoint Address",
|
"label": "Endpoint Address",
|
||||||
"placeholder": "Endpoint Address",
|
"placeholder": "Endpoint Address",
|
||||||
"description": "The endpoint address that peers will connect to."
|
"description": "The endpoint address that peers will connect to. (e.g. wg.example.com or wg.example.com:51820)"
|
||||||
},
|
},
|
||||||
"networks": {
|
"networks": {
|
||||||
"label": "IP Networks",
|
"label": "IP Networks",
|
||||||
|
@@ -38,6 +38,7 @@
|
|||||||
"lang": "Toggle Language",
|
"lang": "Toggle Language",
|
||||||
"profile": "My Profile",
|
"profile": "My Profile",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
|
"audit": "Audit Log",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"logout": "Logout"
|
"logout": "Logout"
|
||||||
},
|
},
|
||||||
@@ -188,6 +189,23 @@
|
|||||||
"api-link": "API Documentation"
|
"api-link": "API Documentation"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"audit": {
|
||||||
|
"headline": "Audit Log",
|
||||||
|
"abstract": "Here you can find the audit log of all actions performed in the WireGuard Portal.",
|
||||||
|
"no-entries": {
|
||||||
|
"headline": "No log entries available",
|
||||||
|
"abstract": "Currently, there are no audit logs recorded."
|
||||||
|
},
|
||||||
|
"entries-headline": "Log Entries",
|
||||||
|
"table-heading": {
|
||||||
|
"id": "#",
|
||||||
|
"time": "Time",
|
||||||
|
"user": "User",
|
||||||
|
"severity": "Severity",
|
||||||
|
"origin": "Origin",
|
||||||
|
"message": "Message"
|
||||||
|
}
|
||||||
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"user-view": {
|
"user-view": {
|
||||||
"headline": "User Account:",
|
"headline": "User Account:",
|
||||||
@@ -355,7 +373,7 @@
|
|||||||
"endpoint": {
|
"endpoint": {
|
||||||
"label": "Endpoint Address",
|
"label": "Endpoint Address",
|
||||||
"placeholder": "Endpoint Address",
|
"placeholder": "Endpoint Address",
|
||||||
"description": "The endpoint address that peers will connect to."
|
"description": "The endpoint address that peers will connect to. (e.g. wg.example.com or wg.example.com:51820)"
|
||||||
},
|
},
|
||||||
"networks": {
|
"networks": {
|
||||||
"label": "IP Networks",
|
"label": "IP Networks",
|
||||||
|
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'
|
import Notifications from '@kyvg/vue3-notification'
|
||||||
|
|
||||||
// Bootstrap (and theme)
|
// Bootstrap (and theme)
|
||||||
//import "bootstrap/dist/css/bootstrap.min.css"
|
import "@/assets/custom.scss";
|
||||||
import "bootswatch/dist/lux/bootstrap.min.css";
|
|
||||||
import "bootstrap";
|
import "bootstrap";
|
||||||
import "./assets/base.css";
|
import "./assets/base.css";
|
||||||
|
|
||||||
// Fontawesome
|
// Fonts
|
||||||
import "@fortawesome/fontawesome-free/js/all.js"
|
import "@fortawesome/fontawesome-free/js/all.js"
|
||||||
|
import "@fontsource/nunito-sans/400.css";
|
||||||
|
import "@fontsource/nunito-sans/600.css";
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
import "flag-icons/css/flag-icons.min.css"
|
import "flag-icons/css/flag-icons.min.css"
|
||||||
|
@@ -4,6 +4,7 @@ import LoginView from '../views/LoginView.vue'
|
|||||||
import InterfaceView from '../views/InterfaceView.vue'
|
import InterfaceView from '../views/InterfaceView.vue'
|
||||||
|
|
||||||
import {authStore} from '@/stores/auth'
|
import {authStore} from '@/stores/auth'
|
||||||
|
import {securityStore} from '@/stores/security'
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -55,6 +56,14 @@ const router = createRouter({
|
|||||||
// this generates a separate chunk (About.[hash].js) for this route
|
// this generates a separate chunk (About.[hash].js) for this route
|
||||||
// which is lazy-loaded when the route is visited.
|
// which is lazy-loaded when the route is visited.
|
||||||
component: () => import('../views/SettingsView.vue')
|
component: () => import('../views/SettingsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/audit',
|
||||||
|
name: 'audit',
|
||||||
|
// 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/AuditView.vue')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
linkActiveClass: "active",
|
linkActiveClass: "active",
|
||||||
@@ -114,4 +123,13 @@ router.beforeEach(async (to) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.afterEach(async (to, from) => {
|
||||||
|
const sec = securityStore()
|
||||||
|
const csrfPages = ['/', '/login']
|
||||||
|
|
||||||
|
if (csrfPages.includes(to.path)) {
|
||||||
|
await sec.LoadSecurityProperties() // make sure we have a valid csrf token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
87
frontend/src/stores/audit.js
Normal file
87
frontend/src/stores/audit.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import {apiWrapper} from "@/helpers/fetch-wrapper";
|
||||||
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
import { base64_url_encode } from '@/helpers/encoding';
|
||||||
|
|
||||||
|
const baseUrl = `/audit`
|
||||||
|
|
||||||
|
export const auditStore = defineStore('audit', {
|
||||||
|
state: () => ({
|
||||||
|
entries: [],
|
||||||
|
filter: "",
|
||||||
|
pageSize: 10,
|
||||||
|
pageOffset: 0,
|
||||||
|
pages: [],
|
||||||
|
fetching: false,
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
Count: (state) => state.entries.length,
|
||||||
|
FilteredCount: (state) => state.Filtered.length,
|
||||||
|
All: (state) => state.entries,
|
||||||
|
Filtered: (state) => {
|
||||||
|
if (!state.filter) {
|
||||||
|
return state.entries
|
||||||
|
}
|
||||||
|
return state.entries.filter((e) => {
|
||||||
|
return e.Timestamp.includes(state.filter) ||
|
||||||
|
e.Message.includes(state.filter) ||
|
||||||
|
e.Severity.includes(state.filter) ||
|
||||||
|
e.Origin.includes(state.filter)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
FilteredAndPaged: (state) => {
|
||||||
|
return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
|
||||||
|
},
|
||||||
|
isFetching: (state) => state.fetching,
|
||||||
|
hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
|
||||||
|
hasPrevPage: (state) => state.pageOffset > 0,
|
||||||
|
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
afterPageSizeChange() {
|
||||||
|
// reset pageOffset to avoid problems with new page sizes
|
||||||
|
this.pageOffset = 0
|
||||||
|
this.calculatePages()
|
||||||
|
},
|
||||||
|
calculatePages() {
|
||||||
|
let pageCounter = 1;
|
||||||
|
this.pages = []
|
||||||
|
for (let i = 0; i < this.FilteredCount; i+=this.pageSize) {
|
||||||
|
this.pages.push(pageCounter++)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gotoPage(page) {
|
||||||
|
this.pageOffset = (page-1) * this.pageSize
|
||||||
|
|
||||||
|
this.calculatePages()
|
||||||
|
},
|
||||||
|
nextPage() {
|
||||||
|
this.pageOffset += this.pageSize
|
||||||
|
|
||||||
|
this.calculatePages()
|
||||||
|
},
|
||||||
|
previousPage() {
|
||||||
|
this.pageOffset -= this.pageSize
|
||||||
|
|
||||||
|
this.calculatePages()
|
||||||
|
},
|
||||||
|
setEntries(entries) {
|
||||||
|
this.entries = entries
|
||||||
|
this.calculatePages()
|
||||||
|
this.fetching = false
|
||||||
|
},
|
||||||
|
async LoadEntries() {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.get(`${baseUrl}/entries`)
|
||||||
|
.then(this.setEntries)
|
||||||
|
.catch(error => {
|
||||||
|
this.setEntries([])
|
||||||
|
console.log("Failed to load audit entries: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to load audit entries!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
@@ -4,8 +4,7 @@ import { notify } from "@kyvg/vue3-notification";
|
|||||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||||
import router from '../router'
|
import router from '../router'
|
||||||
|
|
||||||
export const authStore = defineStore({
|
export const authStore = defineStore('auth',{
|
||||||
id: 'auth',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
// initialize state from local storage to enable user to stay logged in
|
// initialize state from local storage to enable user to stay logged in
|
||||||
user: JSON.parse(localStorage.getItem('user')),
|
user: JSON.parse(localStorage.getItem('user')),
|
||||||
|
@@ -7,8 +7,7 @@ import { base64_url_encode } from '@/helpers/encoding';
|
|||||||
|
|
||||||
const baseUrl = `/interface`
|
const baseUrl = `/interface`
|
||||||
|
|
||||||
export const interfaceStore = defineStore({
|
export const interfaceStore = defineStore('interfaces', {
|
||||||
id: 'interfaces',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
prepared: freshInterface(),
|
prepared: freshInterface(),
|
||||||
|
@@ -8,8 +8,7 @@ import { ipToBigInt } from '@/helpers/utils';
|
|||||||
|
|
||||||
const baseUrl = `/peer`
|
const baseUrl = `/peer`
|
||||||
|
|
||||||
export const peerStore = defineStore({
|
export const peerStore = defineStore('peers', {
|
||||||
id: 'peers',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
peers: [],
|
peers: [],
|
||||||
stats: {},
|
stats: {},
|
||||||
|
@@ -8,10 +8,11 @@ import { ipToBigInt } from '@/helpers/utils';
|
|||||||
|
|
||||||
const baseUrl = `/user`
|
const baseUrl = `/user`
|
||||||
|
|
||||||
export const profileStore = defineStore({
|
export const profileStore = defineStore('profile', {
|
||||||
id: 'profile',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
peers: [],
|
peers: [],
|
||||||
|
interfaces: [],
|
||||||
|
selectedInterfaceId: "",
|
||||||
stats: {},
|
stats: {},
|
||||||
statsEnabled: false,
|
statsEnabled: false,
|
||||||
user: {},
|
user: {},
|
||||||
@@ -71,6 +72,7 @@ export const profileStore = defineStore({
|
|||||||
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
||||||
},
|
},
|
||||||
hasStatistics: (state) => state.statsEnabled,
|
hasStatistics: (state) => state.statsEnabled,
|
||||||
|
CountInterfaces: (state) => state.interfaces.length,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
afterPageSizeChange() {
|
afterPageSizeChange() {
|
||||||
@@ -116,6 +118,11 @@ export const profileStore = defineStore({
|
|||||||
this.stats = statsResponse.Stats
|
this.stats = statsResponse.Stats
|
||||||
this.statsEnabled = statsResponse.Enabled
|
this.statsEnabled = statsResponse.Enabled
|
||||||
},
|
},
|
||||||
|
setInterfaces(interfaces) {
|
||||||
|
this.interfaces = interfaces
|
||||||
|
this.selectedInterfaceId = interfaces.length > 0 ? interfaces[0].Identifier : ""
|
||||||
|
this.fetching = false
|
||||||
|
},
|
||||||
async enableApi() {
|
async enableApi() {
|
||||||
this.fetching = true
|
this.fetching = true
|
||||||
let currentUser = authStore().user.Identifier
|
let currentUser = authStore().user.Identifier
|
||||||
@@ -186,5 +193,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,8 +3,7 @@ import { defineStore } from 'pinia'
|
|||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||||
|
|
||||||
export const securityStore = defineStore({
|
export const securityStore = defineStore('security',{
|
||||||
id: 'security',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
csrfToken: "",
|
csrfToken: "",
|
||||||
}),
|
}),
|
||||||
|
@@ -5,8 +5,7 @@ import { apiWrapper } from '@/helpers/fetch-wrapper'
|
|||||||
|
|
||||||
const baseUrl = `/config`
|
const baseUrl = `/config`
|
||||||
|
|
||||||
export const settingsStore = defineStore({
|
export const settingsStore = defineStore('settings', {
|
||||||
id: 'settings',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
settings: {},
|
settings: {},
|
||||||
}),
|
}),
|
||||||
|
@@ -5,8 +5,7 @@ import { base64_url_encode } from '@/helpers/encoding';
|
|||||||
|
|
||||||
const baseUrl = `/user`
|
const baseUrl = `/user`
|
||||||
|
|
||||||
export const userStore = defineStore({
|
export const userStore = defineStore('users', {
|
||||||
id: 'users',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
userPeers: [],
|
userPeers: [],
|
||||||
users: [],
|
users: [],
|
||||||
|
96
frontend/src/views/AuditView.vue
Normal file
96
frontend/src/views/AuditView.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import {auditStore} from "@/stores/audit";
|
||||||
|
|
||||||
|
const audit = auditStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await audit.LoadEntries()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>{{ $t('audit.headline') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="lead">{{ $t('audit.abstract') }}</p>
|
||||||
|
|
||||||
|
<!-- Entry list -->
|
||||||
|
<div class="mt-4 row">
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<h3>{{ $t('audit.entries-headline') }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-6 text-lg-end">
|
||||||
|
<div class="form-group d-inline">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input v-model="audit.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="audit.afterPageSizeChange">
|
||||||
|
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 table-responsive">
|
||||||
|
<div v-if="audit.Count===0">
|
||||||
|
<h4>{{ $t('audit.no-entries.headline') }}</h4>
|
||||||
|
<p>{{ $t('audit.no-entries.abstract') }}</p>
|
||||||
|
</div>
|
||||||
|
<table v-if="audit.Count!==0" id="auditTable" class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{{ $t('audit.table-heading.id') }}</th>
|
||||||
|
<th class="text-center" scope="col">{{ $t('audit.table-heading.time') }}</th>
|
||||||
|
<th class="text-center" scope="col">{{ $t('audit.table-heading.severity') }}</th>
|
||||||
|
<th scope="col">{{ $t('audit.table-heading.user') }}</th>
|
||||||
|
<th scope="col">{{ $t('audit.table-heading.origin') }}</th>
|
||||||
|
<th scope="col">{{ $t('audit.table-heading.message') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="entry in audit.FilteredAndPaged" :key="entry.Id">
|
||||||
|
<td>{{entry.Id}}</td>
|
||||||
|
<td>{{entry.Timestamp}}</td>
|
||||||
|
<td class="text-center"><span class="badge rounded-pill" :class="[ entry.Severity === 'low' ? 'bg-light' : entry.Severity === 'medium' ? 'bg-warning' : 'bg-danger']">{{entry.Severity}}</span></td>
|
||||||
|
<td>{{entry.ContextUser}}</td>
|
||||||
|
<td>{{entry.Origin}}</td>
|
||||||
|
<td>{{entry.Message}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<ul class="pagination pagination-sm">
|
||||||
|
<li :class="{disabled:audit.pageOffset===0}" class="page-item">
|
||||||
|
<a class="page-link" @click="audit.previousPage">«</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-for="page in audit.pages" :key="page" :class="{active:audit.currentPage===page}" class="page-item">
|
||||||
|
<a class="page-link" @click="audit.gotoPage(page)">{{page}}</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li :class="{disabled:!audit.hasNextPage}" class="page-item">
|
||||||
|
<a class="page-link" @click="audit.nextPage">»</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<select v-model.number="audit.pageSize" class="form-select" @click="audit.afterPageSizeChange()">
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@@ -22,8 +22,9 @@ const multiCreatePeerId = ref("")
|
|||||||
const editInterfaceId = ref("")
|
const editInterfaceId = ref("")
|
||||||
const viewedInterfaceId = ref("")
|
const viewedInterfaceId = ref("")
|
||||||
|
|
||||||
const sortKey = ref("");
|
const sortKey = ref("")
|
||||||
const sortOrder = ref(1);
|
const sortOrder = ref(1)
|
||||||
|
const selectAll = ref(false)
|
||||||
|
|
||||||
function sortBy(key) {
|
function sortBy(key) {
|
||||||
if (sortKey.value === key) {
|
if (sortKey.value === key) {
|
||||||
@@ -48,12 +49,11 @@ async function download() {
|
|||||||
await interfaces.LoadInterfaceConfig(interfaces.GetSelected.Identifier)
|
await interfaces.LoadInterfaceConfig(interfaces.GetSelected.Identifier)
|
||||||
|
|
||||||
// credit: https://www.bitdegree.org/learn/javascript-download
|
// credit: https://www.bitdegree.org/learn/javascript-download
|
||||||
let filename = interfaces.GetSelected.Identifier + ".conf"
|
|
||||||
let text = interfaces.configuration
|
let text = interfaces.configuration
|
||||||
|
|
||||||
let element = document.createElement('a')
|
let element = document.createElement('a')
|
||||||
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
|
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
|
||||||
element.setAttribute('download', filename)
|
element.setAttribute('download', interfaces.GetSelected.Filename)
|
||||||
|
|
||||||
element.style.display = 'none'
|
element.style.display = 'none'
|
||||||
document.body.appendChild(element)
|
document.body.appendChild(element)
|
||||||
@@ -81,6 +81,12 @@ async function saveConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
peers.FilteredAndPaged.forEach(peer => {
|
||||||
|
peer.IsSelected = selectAll.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await interfaces.LoadInterfaces()
|
await interfaces.LoadInterfaces()
|
||||||
await peers.LoadPeers(undefined) // use default interface
|
await peers.LoadPeers(undefined) // use default interface
|
||||||
@@ -326,7 +332,7 @@ onMounted(async () => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
<input id="flexCheckDefault" class="form-check-input" :title="$t('general.select-all')" type="checkbox" value="">
|
<input class="form-check-input" :title="$t('general.select-all')" type="checkbox" v-model="selectAll" @change="toggleSelectAll">
|
||||||
</th><!-- select -->
|
</th><!-- select -->
|
||||||
<th scope="col"></th><!-- status -->
|
<th scope="col"></th><!-- status -->
|
||||||
<th scope="col" @click="sortBy('DisplayName')">
|
<th scope="col" @click="sortBy('DisplayName')">
|
||||||
@@ -357,7 +363,7 @@ onMounted(async () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="peer in peers.FilteredAndPaged" :key="peer.Identifier">
|
<tr v-for="peer in peers.FilteredAndPaged" :key="peer.Identifier">
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
<input class="form-check-input" type="checkbox" v-model="peer.IsSelected">
|
||||||
</th>
|
</th>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span v-if="peer.Disabled" class="text-danger" :title="$t('interfaces.peer-disabled') + ' ' + peer.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
<span v-if="peer.Disabled" class="text-danger" :title="$t('interfaces.peer-disabled') + ' ' + peer.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
||||||
|
@@ -3,7 +3,7 @@ import PeerViewModal from "../components/PeerViewModal.vue";
|
|||||||
|
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { profileStore } from "@/stores/profile";
|
import { profileStore } from "@/stores/profile";
|
||||||
import PeerEditModal from "@/components/PeerEditModal.vue";
|
import UserPeerEditModal from "@/components/UserPeerEditModal.vue";
|
||||||
import { settingsStore } from "@/stores/settings";
|
import { settingsStore } from "@/stores/settings";
|
||||||
import { humanFileSize } from "@/helpers/utils";
|
import { humanFileSize } from "@/helpers/utils";
|
||||||
|
|
||||||
@@ -13,8 +13,9 @@ const profile = profileStore()
|
|||||||
const viewedPeerId = ref("")
|
const viewedPeerId = ref("")
|
||||||
const editPeerId = ref("")
|
const editPeerId = ref("")
|
||||||
|
|
||||||
const sortKey = ref("");
|
const sortKey = ref("")
|
||||||
const sortOrder = ref(1);
|
const sortOrder = ref(1)
|
||||||
|
const selectAll = ref(false)
|
||||||
|
|
||||||
function sortBy(key) {
|
function sortBy(key) {
|
||||||
if (sortKey.value === key) {
|
if (sortKey.value === key) {
|
||||||
@@ -27,10 +28,24 @@ function sortBy(key) {
|
|||||||
profile.sortOrder = sortOrder.value;
|
profile.sortOrder = sortOrder.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function friendlyInterfaceName(id, name) {
|
||||||
|
if (name) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
profile.FilteredAndPagedPeers.forEach(peer => {
|
||||||
|
peer.IsSelected = selectAll.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await profile.LoadUser()
|
await profile.LoadUser()
|
||||||
await profile.LoadPeers()
|
await profile.LoadPeers()
|
||||||
await profile.LoadStats()
|
await profile.LoadStats()
|
||||||
|
await profile.LoadInterfaces()
|
||||||
await profile.calculatePages(); // Forces to show initial page number
|
await profile.calculatePages(); // Forces to show initial page number
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -38,7 +53,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId !== ''" @close="viewedPeerId = ''"></PeerViewModal>
|
<PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId !== ''" @close="viewedPeerId = ''"></PeerViewModal>
|
||||||
<PeerEditModal :peerId="editPeerId" :visible="editPeerId !== ''" @close="editPeerId = ''"></PeerEditModal>
|
<UserPeerEditModal :peerId="editPeerId" :visible="editPeerId !== ''" @close="editPeerId = ''; profile.LoadPeers()"></UserPeerEditModal>
|
||||||
|
|
||||||
<!-- Peer list -->
|
<!-- Peer list -->
|
||||||
<div class="mt-4 row">
|
<div class="mt-4 row">
|
||||||
@@ -56,9 +71,17 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-3 text-lg-end">
|
<div class="col-12 col-lg-3 text-lg-end">
|
||||||
<a v-if="settings.Setting('SelfProvisioning')" class="btn btn-primary ms-2" href="#"
|
<div class="form-group" v-if="settings.Setting('SelfProvisioning')">
|
||||||
:title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'"><i
|
<div class="input-group mb-3">
|
||||||
class="fa fa-plus me-1"></i><i class="fa fa-user"></i></a>
|
<button class="input-group-text btn btn-primary" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'">
|
||||||
|
<i class="fa fa-plus me-1"></i><i class="fa fa-user"></i>
|
||||||
|
</button>
|
||||||
|
<select v-model="profile.selectedInterfaceId" :disabled="profile.CountInterfaces===0" class="form-select">
|
||||||
|
<option v-if="profile.CountInterfaces===0" value="nothing">{{ $t('interfaces.no-interface.default-selection') }}</option>
|
||||||
|
<option v-for="iface in profile.interfaces" :key="iface.Identifier" :value="iface.Identifier">{{ friendlyInterfaceName(iface.Identifier,iface.DisplayName) }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 table-responsive">
|
<div class="mt-2 table-responsive">
|
||||||
@@ -70,8 +93,7 @@ onMounted(async () => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
<input id="flexCheckDefault" class="form-check-input" :title="$t('general.select-all')" type="checkbox"
|
<input class="form-check-input" :title="$t('general.select-all')" type="checkbox" v-model="selectAll" @change="toggleSelectAll">
|
||||||
value="">
|
|
||||||
</th><!-- select -->
|
</th><!-- select -->
|
||||||
<th scope="col"></th><!-- status -->
|
<th scope="col"></th><!-- status -->
|
||||||
<th scope="col" @click="sortBy('DisplayName')">
|
<th scope="col" @click="sortBy('DisplayName')">
|
||||||
@@ -96,7 +118,7 @@ onMounted(async () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="peer in profile.FilteredAndPagedPeers" :key="peer.Identifier">
|
<tr v-for="peer in profile.FilteredAndPagedPeers" :key="peer.Identifier">
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
<input class="form-check-input" type="checkbox" v-model="peer.IsSelected">
|
||||||
</th>
|
</th>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span v-if="peer.Disabled" class="text-danger"><i class="fa fa-circle-xmark"
|
<span v-if="peer.Disabled" class="text-danger"><i class="fa fa-circle-xmark"
|
||||||
|
@@ -1,13 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import PeerViewModal from "../components/PeerViewModal.vue";
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
import { profileStore } from "@/stores/profile";
|
import { profileStore } from "@/stores/profile";
|
||||||
import PeerEditModal from "@/components/PeerEditModal.vue";
|
|
||||||
import { settingsStore } from "@/stores/settings";
|
import { settingsStore } from "@/stores/settings";
|
||||||
import { humanFileSize } from "@/helpers/utils";
|
import { authStore } from "../stores/auth";
|
||||||
import {RouterLink} from "vue-router";
|
|
||||||
import {authStore} from "../stores/auth";
|
|
||||||
|
|
||||||
const profile = profileStore()
|
const profile = profileStore()
|
||||||
const settings = settingsStore()
|
const settings = settingsStore()
|
||||||
|
@@ -3,15 +3,20 @@ import {userStore} from "@/stores/users";
|
|||||||
import {ref,onMounted} from "vue";
|
import {ref,onMounted} from "vue";
|
||||||
import UserEditModal from "../components/UserEditModal.vue";
|
import UserEditModal from "../components/UserEditModal.vue";
|
||||||
import UserViewModal from "../components/UserViewModal.vue";
|
import UserViewModal from "../components/UserViewModal.vue";
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
|
||||||
import {settingsStore} from "@/stores/settings";
|
|
||||||
|
|
||||||
const settings = settingsStore()
|
|
||||||
const users = userStore()
|
const users = userStore()
|
||||||
|
|
||||||
const editUserId = ref("")
|
const editUserId = ref("")
|
||||||
const viewedUserId = ref("")
|
const viewedUserId = ref("")
|
||||||
|
|
||||||
|
const selectAll = ref(false)
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
users.FilteredAndPaged.forEach(user => {
|
||||||
|
user.IsSelected = selectAll.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
users.LoadUsers()
|
users.LoadUsers()
|
||||||
})
|
})
|
||||||
@@ -49,7 +54,7 @@ onMounted(() => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
<input id="flexCheckDefault" class="form-check-input" :title="$t('general.select-all')" type="checkbox" value="">
|
<input class="form-check-input" :title="$t('general.select-all')" type="checkbox" v-model="selectAll" @change="toggleSelectAll">
|
||||||
</th><!-- select -->
|
</th><!-- select -->
|
||||||
<th scope="col"></th><!-- status -->
|
<th scope="col"></th><!-- status -->
|
||||||
<th scope="col">{{ $t('users.table-heading.id') }}</th>
|
<th scope="col">{{ $t('users.table-heading.id') }}</th>
|
||||||
@@ -65,7 +70,7 @@ onMounted(() => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="user in users.FilteredAndPaged" :key="user.Identifier">
|
<tr v-for="user in users.FilteredAndPaged" :key="user.Identifier">
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
<input class="form-check-input" type="checkbox" v-model="user.IsSelected">
|
||||||
</th>
|
</th>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span v-if="user.Disabled" class="text-danger" :title="$t('users.user-disabled') + ' ' + user.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
<span v-if="user.Disabled" class="text-danger" :title="$t('users.user-disabled') + ' ' + user.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
||||||
|
97
go.mod
97
go.mod
@@ -1,32 +1,30 @@
|
|||||||
module github.com/h44z/wg-portal
|
module github.com/h44z/wg-portal
|
||||||
|
|
||||||
go 1.23
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/a8m/envsubst v1.4.2
|
github.com/a8m/envsubst v1.4.3
|
||||||
github.com/coreos/go-oidc/v3 v3.12.0
|
github.com/alexedwards/scs/v2 v2.8.0
|
||||||
github.com/gin-contrib/cors v1.7.3
|
github.com/coreos/go-oidc/v3 v3.14.1
|
||||||
github.com/gin-contrib/sessions v1.0.2
|
|
||||||
github.com/gin-gonic/gin v1.10.0
|
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10
|
github.com/go-ldap/ldap/v3 v3.4.11
|
||||||
|
github.com/go-pkgz/routegroup v1.4.1
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/prometheus-community/pro-bing v0.5.0
|
github.com/prometheus-community/pro-bing v0.7.0
|
||||||
github.com/prometheus/client_golang v1.20.5
|
github.com/prometheus/client_golang v1.22.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/swaggo/swag v1.16.4
|
github.com/swaggo/swag v1.16.4
|
||||||
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f
|
|
||||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
|
|
||||||
github.com/vardius/message-bus v1.1.5
|
github.com/vardius/message-bus v1.1.5
|
||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.4
|
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||||
golang.org/x/crypto v0.32.0
|
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
|
||||||
golang.org/x/oauth2 v0.25.0
|
golang.org/x/crypto v0.37.0
|
||||||
golang.org/x/sys v0.29.0
|
golang.org/x/oauth2 v0.29.0
|
||||||
|
golang.org/x/sys v0.32.0
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/mysql v1.5.7
|
gorm.io/driver/mysql v1.5.7
|
||||||
gorm.io/driver/postgres v1.5.11
|
gorm.io/driver/postgres v1.5.11
|
||||||
gorm.io/driver/sqlserver v1.5.4
|
gorm.io/driver/sqlserver v1.5.4
|
||||||
@@ -38,45 +36,33 @@ require (
|
|||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.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/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // 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
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
github.com/go-openapi/spec v0.21.0 // indirect
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.24.0 // indirect
|
github.com/go-sql-driver/mysql v1.9.2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
|
||||||
github.com/go-test/deep v1.1.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/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.7.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
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
github.com/jackc/pgx/v5 v5.7.4 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.9.0 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@@ -84,35 +70,26 @@ require (
|
|||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/microsoft/go-mssqldb v1.8.0 // indirect
|
github.com/microsoft/go-mssqldb v1.8.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.61.0 // indirect
|
github.com/prometheus/common v0.63.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.16.0 // indirect
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
|
||||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||||
golang.org/x/arch v0.13.0 // indirect
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
golang.org/x/net v0.39.0 // indirect
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/sync v0.13.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.29.0 // indirect
|
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||||
google.golang.org/protobuf v1.36.2 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
modernc.org/libc v1.63.0 // indirect
|
||||||
modernc.org/libc v1.61.7 // indirect
|
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.8.1 // indirect
|
modernc.org/memory v1.10.0 // indirect
|
||||||
modernc.org/sqlite v1.34.4 // indirect
|
modernc.org/sqlite v1.37.0 // indirect
|
||||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
256
go.sum
256
go.sum
@@ -25,85 +25,70 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mx
|
|||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg=
|
github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
|
||||||
github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY=
|
github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
|
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
|
||||||
|
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
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.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.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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||||
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=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
|
||||||
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
|
|
||||||
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
|
|
||||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4=
|
|
||||||
github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963/go.mod h1:4lkInX8nHSR62NSmhXM3xtPeMSyfiR58NaEz+om1lHM=
|
|
||||||
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
|
|
||||||
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
|
|
||||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
|
||||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
|
||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
|
||||||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
|
||||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||||
|
github.com/go-pkgz/routegroup v1.4.1 h1:iw1yW3lXuurZZOv/DF9fY8Mkpvy6J9UjBiP1oDIQE/s=
|
||||||
|
github.com/go-pkgz/routegroup v1.4.1/go.mod h1:kDDPDRLRiRY1vnENrZJw1jQAzQX7fvsbsHGRQFNQfKc=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.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.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
|
||||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
@@ -112,31 +97,19 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
|
|||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
|
||||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
|
||||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
|
||||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
|
||||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
|
||||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
@@ -144,8 +117,10 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
github.com/jackc/pgx/v5 v5.7.3 h1:PO1wNKj/bTAwxSJnO1Z4Ai8j4magtqg2SLNjEDzcXQo=
|
||||||
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
github.com/jackc/pgx/v5 v5.7.3/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||||
|
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||||
@@ -168,21 +143,10 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
|
||||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
@@ -191,7 +155,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||||
@@ -200,56 +163,45 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
|
|||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
|
||||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||||
github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
|
github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
|
||||||
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
|
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||||
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
|
||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.6.1 h1:EQukUOma9YFZRPe4DGSscxUf9LH07rpqwisNWjSZrgU=
|
||||||
github.com/prometheus-community/pro-bing v0.5.0/go.mod h1:1joR9oXdMEAcAJJvhs+8vNDvTg5thfAZcRFhcUozG2g=
|
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-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
|
||||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
|
||||||
|
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||||
|
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||||
|
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||||
|
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||||
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
|
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -261,18 +213,9 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||||
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f h1:oqdnd6OGlOUu1InG37hWcCB3a+Jy3fwjylyVboaNMwY=
|
|
||||||
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f/go.mod h1:X3Dd1SB8Gt1V968NTzpKFjMM6O8ccta2NPC6MprOxZQ=
|
|
||||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 h1:flbMkdl6HxQkLs6DDhH1UkcnFpNBOu70391STjMS0O4=
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5JNfgjzpzvYLHjH0QOgPZPYnRWGA=
|
||||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
|
||||||
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
|
||||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca h1:lpvAjPK+PcxnbcB8H7axIb4fMNwjX9bE4DzwPjGg8aE=
|
|
||||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca/go.mod h1:XXKxNbpoLihvvT7orUZbs/iZayg1n4ip7iJakJPAwA8=
|
|
||||||
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
|
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
|
||||||
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
|
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
|
||||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||||
@@ -282,13 +225,13 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
|||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.4 h1:cXdYlrhzHzVAnJHiwr/T6lAUmS9MtEStjEZBjArrvnc=
|
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.4/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||||
|
github.com/yeqown/go-qrcode/writer/compressed v1.0.1 h1:0el6zOppx3oPiYWMUJWRYGvxWYh8MDmUU0j3rSWGWlI=
|
||||||
|
github.com/yeqown/go-qrcode/writer/compressed v1.0.1/go.mod h1:BJScsGUIKM+eg0CCLCcVaDTaclDM1IEXtq2r8qQnDKk=
|
||||||
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||||
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
|
||||||
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
@@ -302,18 +245,19 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
@@ -331,26 +275,26 @@ 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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||||
|
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -366,8 +310,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -394,34 +338,34 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||||
|
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||||
|
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
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 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
||||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
|
||||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
@@ -438,28 +382,36 @@ gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
|||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
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/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
|
||||||
modernc.org/ccgo/v4 v4.23.10/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0=
|
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
||||||
|
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
|
||||||
|
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
|
||||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=
|
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
||||||
modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/libc v1.61.7 h1:exz8rasFniviSgh3dH7QBnQHqYh9lolA5hVYfsiwkfo=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/libc v1.61.7/go.mod h1:xspSrXRNVSfWfcfqgvZDVe/Hw5kv4FVC6IRfoms5v/0=
|
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
||||||
|
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
||||||
|
modernc.org/libc v1.63.0 h1:wKzb61wOGCzgahQBORb1b0dZonh8Ufzl/7r4Yf1D5YA=
|
||||||
|
modernc.org/libc v1.63.0/go.mod h1:wDzH1mgz1wUIEwottFt++POjGRO9sgyQKrpXaz3x89E=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.8.1 h1:HS1HRg1jEohnuONobEq2WrLEhLyw8+J42yLFTnllm2A=
|
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
|
||||||
modernc.org/memory v1.8.1/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
||||||
|
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
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 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
|
modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=
|
||||||
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
|
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
|
||||||
|
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||||
|
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
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 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
|
||||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||||
|
@@ -4,23 +4,23 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlserver"
|
||||||
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
"gorm.io/gorm/utils"
|
"gorm.io/gorm/utils"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
gormMySQL "gorm.io/driver/mysql"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/driver/sqlserver"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SchemaVersion describes the current database schema version. It must be incremented if a manual migration is needed.
|
// SchemaVersion describes the current database schema version. It must be incremented if a manual migration is needed.
|
||||||
@@ -32,13 +32,15 @@ type SysStat struct {
|
|||||||
SchemaVersion uint64 `gorm:"primaryKey,column:schema_version"`
|
SchemaVersion uint64 `gorm:"primaryKey,column:schema_version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GormLogger is a custom logger for Gorm, making it use logrus.
|
// GormLogger is a custom logger for Gorm, making it use slog
|
||||||
type GormLogger struct {
|
type GormLogger struct {
|
||||||
SlowThreshold time.Duration
|
SlowThreshold time.Duration
|
||||||
SourceField string
|
SourceField string
|
||||||
IgnoreErrRecordNotFound bool
|
IgnoreErrRecordNotFound bool
|
||||||
Debug bool
|
Debug bool
|
||||||
Silent bool
|
Silent bool
|
||||||
|
|
||||||
|
prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogger(slowThreshold time.Duration, debug bool) *GormLogger {
|
func NewLogger(slowThreshold time.Duration, debug bool) *GormLogger {
|
||||||
@@ -48,6 +50,7 @@ func NewLogger(slowThreshold time.Duration, debug bool) *GormLogger {
|
|||||||
IgnoreErrRecordNotFound: true,
|
IgnoreErrRecordNotFound: true,
|
||||||
Silent: false,
|
Silent: false,
|
||||||
SourceField: "src",
|
SourceField: "src",
|
||||||
|
prefix: "GORM-SQL: ",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,25 +63,25 @@ func (l *GormLogger) LogMode(level logger.LogLevel) logger.Interface {
|
|||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Info(ctx context.Context, s string, args ...interface{}) {
|
func (l *GormLogger) Info(ctx context.Context, s string, args ...any) {
|
||||||
if l.Silent {
|
if l.Silent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logrus.WithContext(ctx).Infof(s, args...)
|
slog.InfoContext(ctx, l.prefix+s, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Warn(ctx context.Context, s string, args ...interface{}) {
|
func (l *GormLogger) Warn(ctx context.Context, s string, args ...any) {
|
||||||
if l.Silent {
|
if l.Silent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logrus.WithContext(ctx).Warnf(s, args...)
|
slog.WarnContext(ctx, l.prefix+s, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Error(ctx context.Context, s string, args ...interface{}) {
|
func (l *GormLogger) Error(ctx context.Context, s string, args ...any) {
|
||||||
if l.Silent {
|
if l.Silent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logrus.WithContext(ctx).Errorf(s, args...)
|
slog.ErrorContext(ctx, l.prefix+s, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
||||||
@@ -88,36 +91,40 @@ func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (stri
|
|||||||
|
|
||||||
elapsed := time.Since(begin)
|
elapsed := time.Since(begin)
|
||||||
sql, rows := fc()
|
sql, rows := fc()
|
||||||
fields := logrus.Fields{
|
|
||||||
"rows": rows,
|
attrs := []any{
|
||||||
"duration": elapsed,
|
"rows", rows,
|
||||||
|
"duration", elapsed,
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.SourceField != "" {
|
if l.SourceField != "" {
|
||||||
fields[l.SourceField] = utils.FileWithLineNum()
|
attrs = append(attrs, l.SourceField, utils.FileWithLineNum())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil && !(errors.Is(err, gorm.ErrRecordNotFound) && l.IgnoreErrRecordNotFound) {
|
if err != nil && !(errors.Is(err, gorm.ErrRecordNotFound) && l.IgnoreErrRecordNotFound) {
|
||||||
fields[logrus.ErrorKey] = err
|
attrs = append(attrs, "error", err)
|
||||||
logrus.WithContext(ctx).WithFields(fields).Errorf("%s", sql)
|
slog.ErrorContext(ctx, l.prefix+sql, attrs...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.SlowThreshold != 0 && elapsed > l.SlowThreshold {
|
if l.SlowThreshold != 0 && elapsed > l.SlowThreshold {
|
||||||
logrus.WithContext(ctx).WithFields(fields).Warnf("%s", sql)
|
slog.WarnContext(ctx, l.prefix+sql, attrs...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.Debug {
|
if l.Debug {
|
||||||
logrus.WithContext(ctx).WithFields(fields).Tracef("%s", sql)
|
slog.DebugContext(ctx, l.prefix+sql, attrs...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewDatabase creates a new database connection and returns a Gorm database instance.
|
||||||
func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
|
func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
|
||||||
var gormDb *gorm.DB
|
var gormDb *gorm.DB
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch cfg.Type {
|
switch cfg.Type {
|
||||||
case config.DatabaseMySQL:
|
case config.DatabaseMySQL:
|
||||||
gormDb, err = gorm.Open(gormMySQL.Open(cfg.DSN), &gorm.Config{
|
gormDb, err = gorm.Open(mysql.Open(cfg.DSN), &gorm.Config{
|
||||||
Logger: NewLogger(cfg.SlowQueryThreshold, cfg.Debug),
|
Logger: NewLogger(cfg.SlowQueryThreshold, cfg.Debug),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -172,6 +179,7 @@ type SqlRepo struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSqlRepository creates a new SqlRepo instance.
|
||||||
func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) {
|
func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) {
|
||||||
repo := &SqlRepo{
|
repo := &SqlRepo{
|
||||||
db: db,
|
db: db,
|
||||||
@@ -210,13 +218,13 @@ func (r *SqlRepo) preCheck() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *SqlRepo) migrate() error {
|
func (r *SqlRepo) migrate() error {
|
||||||
logrus.Tracef("sysstat migration: %v", r.db.AutoMigrate(&SysStat{}))
|
slog.Debug("running migration: sys-stat", "result", r.db.AutoMigrate(&SysStat{}))
|
||||||
logrus.Tracef("user migration: %v", r.db.AutoMigrate(&domain.User{}))
|
slog.Debug("running migration: user", "result", r.db.AutoMigrate(&domain.User{}))
|
||||||
logrus.Tracef("interface migration: %v", r.db.AutoMigrate(&domain.Interface{}))
|
slog.Debug("running migration: interface", "result", r.db.AutoMigrate(&domain.Interface{}))
|
||||||
logrus.Tracef("peer migration: %v", r.db.AutoMigrate(&domain.Peer{}))
|
slog.Debug("running migration: peer", "result", r.db.AutoMigrate(&domain.Peer{}))
|
||||||
logrus.Tracef("peer status migration: %v", r.db.AutoMigrate(&domain.PeerStatus{}))
|
slog.Debug("running migration: peer status", "result", r.db.AutoMigrate(&domain.PeerStatus{}))
|
||||||
logrus.Tracef("interface status migration: %v", r.db.AutoMigrate(&domain.InterfaceStatus{}))
|
slog.Debug("running migration: interface status", "result", r.db.AutoMigrate(&domain.InterfaceStatus{}))
|
||||||
logrus.Tracef("audit data migration: %v", r.db.AutoMigrate(&domain.AuditEntry{}))
|
slog.Debug("running migration: audit data", "result", r.db.AutoMigrate(&domain.AuditEntry{}))
|
||||||
|
|
||||||
existingSysStat := SysStat{}
|
existingSysStat := SysStat{}
|
||||||
r.db.Where("schema_version = ?", SchemaVersion).First(&existingSysStat)
|
r.db.Where("schema_version = ?", SchemaVersion).First(&existingSysStat)
|
||||||
@@ -228,7 +236,7 @@ func (r *SqlRepo) migrate() error {
|
|||||||
if err := r.db.Create(&sysStat).Error; err != nil {
|
if err := r.db.Create(&sysStat).Error; err != nil {
|
||||||
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", SchemaVersion, err)
|
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", SchemaVersion, err)
|
||||||
}
|
}
|
||||||
logrus.Debugf("sysstat entry for schema version %d written", SchemaVersion)
|
slog.Debug("sys-stat entry written", "schema_version", SchemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -236,6 +244,8 @@ func (r *SqlRepo) migrate() error {
|
|||||||
|
|
||||||
// region interfaces
|
// region interfaces
|
||||||
|
|
||||||
|
// GetInterface returns the interface with the given id.
|
||||||
|
// If no interface is found, an error domain.ErrNotFound is returned.
|
||||||
func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
|
func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
|
||||||
var in domain.Interface
|
var in domain.Interface
|
||||||
|
|
||||||
@@ -251,6 +261,8 @@ func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifie
|
|||||||
return &in, nil
|
return &in, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterfaceAndPeers returns the interface with the given id and all peers associated with it.
|
||||||
|
// If no interface is found, an error domain.ErrNotFound is returned.
|
||||||
func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
|
func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
*domain.Interface,
|
*domain.Interface,
|
||||||
[]domain.Peer,
|
[]domain.Peer,
|
||||||
@@ -269,6 +281,7 @@ func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceI
|
|||||||
return in, peers, nil
|
return in, peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeersStats returns the stats for the given peer ids. The order of the returned stats is not guaranteed.
|
||||||
func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) {
|
func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) {
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -284,6 +297,7 @@ func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifie
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllInterfaces returns all interfaces.
|
||||||
func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
|
func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
|
||||||
var interfaces []domain.Interface
|
var interfaces []domain.Interface
|
||||||
|
|
||||||
@@ -295,6 +309,8 @@ func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, err
|
|||||||
return interfaces, nil
|
return interfaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterfaceStats returns the stats for the given interface id.
|
||||||
|
// If no stats are found, an error domain.ErrNotFound is returned.
|
||||||
func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) (
|
func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
*domain.InterfaceStatus,
|
*domain.InterfaceStatus,
|
||||||
error,
|
error,
|
||||||
@@ -319,6 +335,8 @@ func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIden
|
|||||||
return &stat, nil
|
return &stat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindInterfaces returns all interfaces that match the given search string.
|
||||||
|
// The search string is matched against the interface identifier and display name.
|
||||||
func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) {
|
func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) {
|
||||||
var users []domain.Interface
|
var users []domain.Interface
|
||||||
|
|
||||||
@@ -335,6 +353,7 @@ func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.I
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveInterface updates the interface with the given id.
|
||||||
func (r *SqlRepo) SaveInterface(
|
func (r *SqlRepo) SaveInterface(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.InterfaceIdentifier,
|
id domain.InterfaceIdentifier,
|
||||||
@@ -410,6 +429,7 @@ func (r *SqlRepo) upsertInterface(ui *domain.ContextUserInfo, tx *gorm.DB, in *d
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteInterface deletes the interface with the given id.
|
||||||
func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
|
func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
err := tx.Where("interface_identifier = ?", id).Delete(&domain.Peer{}).Error
|
err := tx.Where("interface_identifier = ?", id).Delete(&domain.Peer{}).Error
|
||||||
@@ -436,6 +456,7 @@ func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdenti
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterfaceIps returns a map of interface identifiers to their respective IP addresses.
|
||||||
func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) {
|
func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) {
|
||||||
var ips []struct {
|
var ips []struct {
|
||||||
domain.Cidr
|
domain.Cidr
|
||||||
@@ -461,6 +482,8 @@ func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIden
|
|||||||
|
|
||||||
// region peers
|
// region peers
|
||||||
|
|
||||||
|
// GetPeer returns the peer with the given id.
|
||||||
|
// If no peer is found, an error domain.ErrNotFound is returned.
|
||||||
func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
|
func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
|
||||||
var peer domain.Peer
|
var peer domain.Peer
|
||||||
|
|
||||||
@@ -476,6 +499,7 @@ func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domai
|
|||||||
return &peer, nil
|
return &peer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterfacePeers returns all peers associated with the given interface id.
|
||||||
func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
|
func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
|
||||||
var peers []domain.Peer
|
var peers []domain.Peer
|
||||||
|
|
||||||
@@ -487,6 +511,8 @@ func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIden
|
|||||||
return peers, nil
|
return peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindInterfacePeers returns all peers associated with the given interface id that match the given search string.
|
||||||
|
// The search string is matched against the peer identifier, display name and IP address.
|
||||||
func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) (
|
func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) (
|
||||||
[]domain.Peer,
|
[]domain.Peer,
|
||||||
error,
|
error,
|
||||||
@@ -506,6 +532,7 @@ func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIde
|
|||||||
return peers, nil
|
return peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserPeers returns all peers associated with the given user id.
|
||||||
func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
||||||
var peers []domain.Peer
|
var peers []domain.Peer
|
||||||
|
|
||||||
@@ -517,6 +544,8 @@ func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([
|
|||||||
return peers, nil
|
return peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindUserPeers returns all peers associated with the given user id that match the given search string.
|
||||||
|
// The search string is matched against the peer identifier, display name and IP address.
|
||||||
func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, search string) ([]domain.Peer, error) {
|
func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, search string) ([]domain.Peer, error) {
|
||||||
var peers []domain.Peer
|
var peers []domain.Peer
|
||||||
|
|
||||||
@@ -533,6 +562,8 @@ func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, s
|
|||||||
return peers, nil
|
return peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SavePeer updates the peer with the given id.
|
||||||
|
// If no existing peer is found, a new peer is created.
|
||||||
func (r *SqlRepo) SavePeer(
|
func (r *SqlRepo) SavePeer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.PeerIdentifier,
|
id domain.PeerIdentifier,
|
||||||
@@ -607,6 +638,7 @@ func (r *SqlRepo) upsertPeer(ui *domain.ContextUserInfo, tx *gorm.DB, peer *doma
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeletePeer deletes the peer with the given id.
|
||||||
func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error {
|
func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error {
|
||||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
err := tx.Delete(&domain.PeerStatus{PeerId: id}).Error
|
err := tx.Delete(&domain.PeerStatus{PeerId: id}).Error
|
||||||
@@ -628,6 +660,7 @@ func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeerIps returns a map of peer identifiers to their respective IP addresses.
|
||||||
func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]domain.Cidr, error) {
|
func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]domain.Cidr, error) {
|
||||||
var ips []struct {
|
var ips []struct {
|
||||||
domain.Cidr
|
domain.Cidr
|
||||||
@@ -649,6 +682,7 @@ func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]d
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsedIpsPerSubnet returns a map of subnets to their respective used IP addresses.
|
||||||
func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (
|
func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (
|
||||||
map[domain.Cidr][]domain.Cidr,
|
map[domain.Cidr][]domain.Cidr,
|
||||||
error,
|
error,
|
||||||
@@ -707,6 +741,8 @@ func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr
|
|||||||
|
|
||||||
// region users
|
// region users
|
||||||
|
|
||||||
|
// GetUser returns the user with the given id.
|
||||||
|
// If no user is found, an error domain.ErrNotFound is returned.
|
||||||
func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||||
var user domain.User
|
var user domain.User
|
||||||
|
|
||||||
@@ -722,6 +758,9 @@ func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domai
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail returns the user with the given email.
|
||||||
|
// If no user is found, an error domain.ErrNotFound is returned.
|
||||||
|
// If multiple users are found, an error domain.ErrNotUnique is returned.
|
||||||
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
|
|
||||||
@@ -746,6 +785,7 @@ func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.Use
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllUsers returns all users.
|
||||||
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
|
|
||||||
@@ -757,6 +797,8 @@ func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindUsers returns all users that match the given search string.
|
||||||
|
// The search string is matched against the user identifier, firstname, lastname and email.
|
||||||
func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User, error) {
|
func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User, error) {
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
|
|
||||||
@@ -774,6 +816,8 @@ func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User,
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveUser updates the user with the given id.
|
||||||
|
// If no user is found, a new user is created.
|
||||||
func (r *SqlRepo) SaveUser(
|
func (r *SqlRepo) SaveUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.UserIdentifier,
|
id domain.UserIdentifier,
|
||||||
@@ -807,6 +851,7 @@ func (r *SqlRepo) SaveUser(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteUser deletes the user with the given id.
|
||||||
func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) error {
|
func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) error {
|
||||||
err := r.db.WithContext(ctx).Delete(&domain.User{}, id).Error
|
err := r.db.WithContext(ctx).Delete(&domain.User{}, id).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -859,6 +904,8 @@ func (r *SqlRepo) upsertUser(ui *domain.ContextUserInfo, tx *gorm.DB, user *doma
|
|||||||
|
|
||||||
// region statistics
|
// region statistics
|
||||||
|
|
||||||
|
// UpdateInterfaceStatus updates the interface status with the given id.
|
||||||
|
// If no interface status is found, a new one is created.
|
||||||
func (r *SqlRepo) UpdateInterfaceStatus(
|
func (r *SqlRepo) UpdateInterfaceStatus(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.InterfaceIdentifier,
|
id domain.InterfaceIdentifier,
|
||||||
@@ -919,6 +966,8 @@ func (r *SqlRepo) upsertInterfaceStatus(tx *gorm.DB, in *domain.InterfaceStatus)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdatePeerStatus updates the peer status with the given id.
|
||||||
|
// If no peer status is found, a new one is created.
|
||||||
func (r *SqlRepo) UpdatePeerStatus(
|
func (r *SqlRepo) UpdatePeerStatus(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.PeerIdentifier,
|
id domain.PeerIdentifier,
|
||||||
@@ -976,6 +1025,7 @@ func (r *SqlRepo) upsertPeerStatus(tx *gorm.DB, in *domain.PeerStatus) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeletePeerStatus deletes the peer status with the given id.
|
||||||
func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier) error {
|
func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier) error {
|
||||||
err := r.db.WithContext(ctx).Delete(&domain.PeerStatus{}, id).Error
|
err := r.db.WithContext(ctx).Delete(&domain.PeerStatus{}, id).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -989,6 +1039,7 @@ func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier
|
|||||||
|
|
||||||
// region audit
|
// region audit
|
||||||
|
|
||||||
|
// SaveAuditEntry saves the given audit entry.
|
||||||
func (r *SqlRepo) SaveAuditEntry(ctx context.Context, entry *domain.AuditEntry) error {
|
func (r *SqlRepo) SaveAuditEntry(ctx context.Context, entry *domain.AuditEntry) error {
|
||||||
err := r.db.WithContext(ctx).Save(entry).Error
|
err := r.db.WithContext(ctx).Save(entry).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -998,4 +1049,16 @@ func (r *SqlRepo) SaveAuditEntry(ctx context.Context, entry *domain.AuditEntry)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllAuditEntries retrieves all audit entries from the database.
|
||||||
|
// The entries are ordered by timestamp, with the newest entries first.
|
||||||
|
func (r *SqlRepo) GetAllAuditEntries(ctx context.Context) ([]domain.AuditEntry, error) {
|
||||||
|
var entries []domain.AuditEntry
|
||||||
|
err := r.db.WithContext(ctx).Order("created_at desc").Find(&entries).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
// endregion audit
|
// endregion audit
|
||||||
|
@@ -5,17 +5,14 @@ package adapters
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func tempSqliteDb(t *testing.T) *gorm.DB {
|
func tempSqliteDb(t *testing.T) *gorm.DB {
|
||||||
|
|
||||||
// github.com/mattn/go-sqlite3
|
|
||||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@@ -2,8 +2,8 @@ package adapters
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
@@ -12,6 +12,7 @@ type FilesystemRepo struct {
|
|||||||
basePath string
|
basePath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewFileSystemRepository creates a new FilesystemRepo instance.
|
||||||
func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) {
|
func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) {
|
||||||
if basePath == "" {
|
if basePath == "" {
|
||||||
return nil, nil // no path, return empty repository
|
return nil, nil // no path, return empty repository
|
||||||
@@ -26,6 +27,10 @@ func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) {
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteFile writes the given contents to the given path.
|
||||||
|
// The path is relative to the base path of the repository.
|
||||||
|
// If the parent directory does not exist, it is created.
|
||||||
|
// If the file already exists, it is overwritten.
|
||||||
func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
|
func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
|
||||||
filePath := filepath.Join(r.basePath, path)
|
filePath := filepath.Join(r.basePath, path)
|
||||||
parentDirectory := filepath.Dir(filePath)
|
parentDirectory := filepath.Dir(filePath)
|
||||||
@@ -36,11 +41,11 @@ func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
|
|||||||
|
|
||||||
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm)
|
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open file %s: %w", file.Name(), err)
|
return fmt.Errorf("failed to open file %s: %w", filePath, err)
|
||||||
}
|
}
|
||||||
defer func(file *os.File) {
|
defer func(file *os.File) {
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
logrus.Errorf("failed to close file %s: %v", file.Name(), err)
|
slog.Error("failed to close file", "file", file.Name(), "error", err)
|
||||||
}
|
}
|
||||||
}(file)
|
}(file)
|
||||||
|
|
||||||
@@ -50,5 +55,17 @@ func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteFile deletes the file at the given path.
|
||||||
|
// The path is relative to the base path of the repository.
|
||||||
|
// If the file does not exist, it is ignored.
|
||||||
|
func (r *FilesystemRepo) DeleteFile(path string) error {
|
||||||
|
filePath := filepath.Join(r.basePath, path)
|
||||||
|
|
||||||
|
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to delete file %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -5,23 +5,26 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mail "github.com/xhit/go-simple-mail/v2"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
"github.com/h44z/wg-portal/internal"
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
mail "github.com/xhit/go-simple-mail/v2"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type MailRepo struct {
|
type MailRepo struct {
|
||||||
cfg *config.MailConfig
|
cfg *config.MailConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSmtpMailRepo creates a new MailRepo instance.
|
||||||
func NewSmtpMailRepo(cfg config.MailConfig) MailRepo {
|
func NewSmtpMailRepo(cfg config.MailConfig) MailRepo {
|
||||||
return MailRepo{cfg: &cfg}
|
return MailRepo{cfg: &cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send sends a mail.
|
// Send sends a mail using SMTP.
|
||||||
func (r MailRepo) Send(_ context.Context, subject, body string, to []string, options *domain.MailOptions) error {
|
func (r MailRepo) Send(_ context.Context, subject, body string, to []string, options *domain.MailOptions) error {
|
||||||
if options == nil {
|
if options == nil {
|
||||||
options = &domain.MailOptions{}
|
options = &domain.MailOptions{}
|
||||||
|
@@ -2,16 +2,18 @@ package adapters
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
"github.com/h44z/wg-portal/internal"
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetricsServer struct {
|
type MetricsServer struct {
|
||||||
@@ -84,16 +86,16 @@ func NewMetricsServer(cfg *config.Config) *MetricsServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the metrics server
|
// Run starts the metrics server. The function blocks until the context is cancelled.
|
||||||
func (m *MetricsServer) Run(ctx context.Context) {
|
func (m *MetricsServer) Run(ctx context.Context) {
|
||||||
// Run the metrics server in a goroutine
|
// Run the metrics server in a goroutine
|
||||||
go func() {
|
go func() {
|
||||||
if err := m.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := m.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
logrus.Errorf("metrics service on %s exited: %v", m.Addr, err)
|
slog.Error("metrics service exited", "address", m.Addr, "error", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
logrus.Infof("started metrics service on %s", m.Addr)
|
slog.Info("started metrics service", "address", m.Addr)
|
||||||
|
|
||||||
// Wait for the context to be done
|
// Wait for the context to be done
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
@@ -102,11 +104,11 @@ func (m *MetricsServer) Run(ctx context.Context) {
|
|||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Attempt to gracefully shutdown the metrics server
|
// Attempt to gracefully shut down the metrics server
|
||||||
if err := m.Shutdown(shutdownCtx); err != nil {
|
if err := m.Shutdown(shutdownCtx); err != nil {
|
||||||
logrus.Errorf("metrics service on %s shutdown failed: %v", m.Addr, err)
|
slog.Error("metrics service shutdown failed", "address", m.Addr, "error", err)
|
||||||
} else {
|
} else {
|
||||||
logrus.Infof("metrics service on %s shutdown gracefully", m.Addr)
|
slog.Info("metrics service shutdown gracefully", "address", m.Addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +123,9 @@ func (m *MetricsServer) UpdateInterfaceMetrics(status domain.InterfaceStatus) {
|
|||||||
func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus) {
|
func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus) {
|
||||||
labels := []string{
|
labels := []string{
|
||||||
string(peer.InterfaceIdentifier),
|
string(peer.InterfaceIdentifier),
|
||||||
string(peer.Interface.AddressStr()),
|
peer.Interface.AddressStr(),
|
||||||
string(status.PeerId),
|
string(status.PeerId),
|
||||||
string(peer.DisplayName),
|
peer.DisplayName,
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.LastHandshake != nil {
|
if status.LastHandshake != nil {
|
||||||
|
@@ -3,12 +3,12 @@ package adapters
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
"github.com/h44z/wg-portal/internal"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// WgQuickRepo implements higher level wg-quick like interactions like setting DNS, routing tables or interface hooks.
|
// WgQuickRepo implements higher level wg-quick like interactions like setting DNS, routing tables or interface hooks.
|
||||||
@@ -17,6 +17,7 @@ type WgQuickRepo struct {
|
|||||||
resolvConfIfacePrefix string
|
resolvConfIfacePrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewWgQuickRepo creates a new WgQuickRepo instance.
|
||||||
func NewWgQuickRepo() *WgQuickRepo {
|
func NewWgQuickRepo() *WgQuickRepo {
|
||||||
return &WgQuickRepo{
|
return &WgQuickRepo{
|
||||||
shellCmd: "bash",
|
shellCmd: "bash",
|
||||||
@@ -24,12 +25,16 @@ func NewWgQuickRepo() *WgQuickRepo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecuteInterfaceHook executes the given hook command.
|
||||||
|
// The hook command can contain the following placeholders:
|
||||||
|
//
|
||||||
|
// %i: the interface identifier.
|
||||||
func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error {
|
func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error {
|
||||||
if hookCmd == "" {
|
if hookCmd == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Tracef("interface %s: executing hook %s", id, hookCmd)
|
slog.Debug("executing interface hook", "interface", id, "hook", hookCmd)
|
||||||
err := r.exec(hookCmd, id)
|
err := r.exec(hookCmd, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to exec hook: %w", err)
|
return fmt.Errorf("failed to exec hook: %w", err)
|
||||||
@@ -38,6 +43,7 @@ func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCm
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDNS sets the DNS settings for the given interface. It uses resolvconf to set the DNS settings.
|
||||||
func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error {
|
func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error {
|
||||||
if dnsStr == "" && dnsSearchStr == "" {
|
if dnsStr == "" && dnsSearchStr == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -67,6 +73,7 @@ func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnsetDNS unsets the DNS settings for the given interface. It uses resolvconf to unset the DNS settings.
|
||||||
func (r *WgQuickRepo) UnsetDNS(id domain.InterfaceIdentifier) error {
|
func (r *WgQuickRepo) UnsetDNS(id domain.InterfaceIdentifier) error {
|
||||||
dnsCommand := "resolvconf -d %resPref%i -f"
|
dnsCommand := "resolvconf -d %resPref%i -f"
|
||||||
|
|
||||||
@@ -99,6 +106,8 @@ func (r *WgQuickRepo) exec(command string, interfaceId domain.InterfaceIdentifie
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to exexute shell command %s: %w", commandWithInterfaceName, err)
|
return fmt.Errorf("failed to exexute shell command %s: %w", commandWithInterfaceName, err)
|
||||||
}
|
}
|
||||||
logrus.Tracef("executed shell command %s, with output: %s", commandWithInterfaceName, string(out))
|
slog.Debug("executed shell command",
|
||||||
|
"command", commandWithInterfaceName,
|
||||||
|
"output", string(out))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -6,11 +6,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
|
||||||
"github.com/h44z/wg-portal/internal/lowlevel"
|
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl"
|
"golang.zx2c4.com/wireguard/wgctrl"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
"github.com/h44z/wg-portal/internal/lowlevel"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WgRepo implements all low-level WireGuard interactions.
|
// WgRepo implements all low-level WireGuard interactions.
|
||||||
@@ -19,6 +20,8 @@ type WgRepo struct {
|
|||||||
nl lowlevel.NetlinkClient
|
nl lowlevel.NetlinkClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewWireGuardRepository creates a new WgRepo instance.
|
||||||
|
// This repository is used to interact with the WireGuard kernel or userspace module.
|
||||||
func NewWireGuardRepository() *WgRepo {
|
func NewWireGuardRepository() *WgRepo {
|
||||||
wg, err := wgctrl.New()
|
wg, err := wgctrl.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -35,6 +38,7 @@ func NewWireGuardRepository() *WgRepo {
|
|||||||
return repo
|
return repo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterfaces returns all existing WireGuard interfaces.
|
||||||
func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
||||||
devices, err := r.wg.Devices()
|
devices, err := r.wg.Devices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -53,10 +57,14 @@ func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, e
|
|||||||
return interfaces, nil
|
return interfaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterface returns the interface with the given id.
|
||||||
|
// If no interface is found, an error os.ErrNotExist is returned.
|
||||||
func (r *WgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
func (r *WgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
||||||
return r.getInterface(id)
|
return r.getInterface(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeers returns all peers associated with the given interface id.
|
||||||
|
// If the requested interface is found, an error os.ErrNotExist is returned.
|
||||||
func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) {
|
func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) {
|
||||||
device, err := r.wg.Device(string(deviceId))
|
device, err := r.wg.Device(string(deviceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -75,6 +83,8 @@ func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier
|
|||||||
return peers, nil
|
return peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeer returns the peer with the given id.
|
||||||
|
// If the requested interface or peer is found, an error os.ErrNotExist is returned.
|
||||||
func (r *WgRepo) GetPeer(
|
func (r *WgRepo) GetPeer(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
deviceId domain.InterfaceIdentifier,
|
deviceId domain.InterfaceIdentifier,
|
||||||
@@ -156,6 +166,9 @@ func (r *WgRepo) convertWireGuardPeer(peer *wgtypes.Peer) (domain.PhysicalPeer,
|
|||||||
return peerModel, nil
|
return peerModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveInterface updates the interface with the given id.
|
||||||
|
// If no existing interface is found, a new interface is created.
|
||||||
|
// Updating the interface does not interrupt any existing connections.
|
||||||
func (r *WgRepo) SaveInterface(
|
func (r *WgRepo) SaveInterface(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
id domain.InterfaceIdentifier,
|
id domain.InterfaceIdentifier,
|
||||||
@@ -186,10 +199,10 @@ func (r *WgRepo) SaveInterface(
|
|||||||
func (r *WgRepo) getOrCreateInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
func (r *WgRepo) getOrCreateInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
||||||
device, err := r.getInterface(id)
|
device, err := r.getInterface(id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return device, nil
|
return device, nil // interface exists
|
||||||
}
|
}
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
return nil, fmt.Errorf("device error: %w", err)
|
return nil, fmt.Errorf("device error: %w", err) // unknown error
|
||||||
}
|
}
|
||||||
|
|
||||||
// create new device
|
// create new device
|
||||||
@@ -307,6 +320,8 @@ func (r *WgRepo) updateWireGuardInterface(pi *domain.PhysicalInterface) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteInterface deletes the interface with the given id.
|
||||||
|
// If the requested interface is found, no error is returned.
|
||||||
func (r *WgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
func (r *WgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
||||||
if err := r.deleteLowLevelInterface(id); err != nil {
|
if err := r.deleteLowLevelInterface(id); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -333,6 +348,8 @@ func (r *WgRepo) deleteLowLevelInterface(id domain.InterfaceIdentifier) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SavePeer updates the peer with the given id.
|
||||||
|
// If no existing peer is found, a new peer is created.
|
||||||
func (r *WgRepo) SavePeer(
|
func (r *WgRepo) SavePeer(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
deviceId domain.InterfaceIdentifier,
|
deviceId domain.InterfaceIdentifier,
|
||||||
@@ -362,10 +379,10 @@ func (r *WgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.
|
|||||||
) {
|
) {
|
||||||
peer, err := r.getPeer(deviceId, id)
|
peer, err := r.getPeer(deviceId, id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return peer, nil
|
return peer, nil // peer exists
|
||||||
}
|
}
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
return nil, fmt.Errorf("peer error: %w", err)
|
return nil, fmt.Errorf("peer error: %w", err) // unknown error
|
||||||
}
|
}
|
||||||
|
|
||||||
// create new peer
|
// create new peer
|
||||||
@@ -424,6 +441,8 @@ func (r *WgRepo) updatePeer(deviceId domain.InterfaceIdentifier, pp *domain.Phys
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeletePeer deletes the peer with the given id.
|
||||||
|
// If the requested interface or peer is found, no error is returned.
|
||||||
func (r *WgRepo) DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
|
func (r *WgRepo) DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
|
||||||
if !id.IsPublicKey() {
|
if !id.IsPublicKey() {
|
||||||
return errors.New("invalid public key")
|
return errors.New("invalid public key")
|
||||||
|
@@ -12,11 +12,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// setup WireGuard manager with no linked store
|
// setup WireGuard manager with no linked store
|
||||||
@@ -43,12 +43,12 @@ func Test_wgRepository_GetInterfaces(t *testing.T) {
|
|||||||
mgr := setup(t)
|
mgr := setup(t)
|
||||||
|
|
||||||
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
||||||
defer mgr.DeleteInterface(context.Background(), interfaceName)
|
defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName))
|
||||||
err := mgr.SaveInterface(context.Background(), interfaceName, nil)
|
err := mgr.SaveInterface(context.Background(), interfaceName, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
interfaceName2 := domain.InterfaceIdentifier("wg_test_002")
|
interfaceName2 := domain.InterfaceIdentifier("wg_test_002")
|
||||||
defer mgr.DeleteInterface(context.Background(), interfaceName2)
|
defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName2))
|
||||||
err = mgr.SaveInterface(context.Background(), interfaceName2, nil)
|
err = mgr.SaveInterface(context.Background(), interfaceName2, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -66,15 +66,16 @@ func TestWireGuardCreateInterface(t *testing.T) {
|
|||||||
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
||||||
ipAddress := "10.11.12.13"
|
ipAddress := "10.11.12.13"
|
||||||
ipV6Address := "1337:d34d:b33f::2"
|
ipV6Address := "1337:d34d:b33f::2"
|
||||||
defer mgr.DeleteInterface(context.Background(), interfaceName)
|
defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName))
|
||||||
|
|
||||||
err := mgr.SaveInterface(context.Background(), interfaceName, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
err := mgr.SaveInterface(context.Background(), interfaceName,
|
||||||
pi.Addresses = []domain.Cidr{
|
func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
||||||
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipAddress), Mask: net.CIDRMask(24, 32)}),
|
pi.Addresses = []domain.Cidr{
|
||||||
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipV6Address), Mask: net.CIDRMask(64, 128)}),
|
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipAddress), Mask: net.CIDRMask(24, 32)}),
|
||||||
}
|
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipV6Address), Mask: net.CIDRMask(64, 128)}),
|
||||||
return pi, nil
|
}
|
||||||
})
|
return pi, nil
|
||||||
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Validate that the interface has been created
|
// Validate that the interface has been created
|
||||||
@@ -90,7 +91,7 @@ func TestWireGuardUpdateInterface(t *testing.T) {
|
|||||||
mgr := setup(t)
|
mgr := setup(t)
|
||||||
|
|
||||||
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
||||||
defer mgr.DeleteInterface(context.Background(), interfaceName)
|
defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName))
|
||||||
|
|
||||||
err := mgr.SaveInterface(context.Background(), interfaceName, nil)
|
err := mgr.SaveInterface(context.Background(), interfaceName, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -102,13 +103,14 @@ func TestWireGuardUpdateInterface(t *testing.T) {
|
|||||||
|
|
||||||
ipAddress := "10.11.12.13"
|
ipAddress := "10.11.12.13"
|
||||||
ipV6Address := "1337:d34d:b33f::2"
|
ipV6Address := "1337:d34d:b33f::2"
|
||||||
err = mgr.SaveInterface(context.Background(), interfaceName, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
err = mgr.SaveInterface(context.Background(), interfaceName,
|
||||||
pi.Addresses = []domain.Cidr{
|
func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
||||||
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipAddress), Mask: net.CIDRMask(24, 32)}),
|
pi.Addresses = []domain.Cidr{
|
||||||
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipV6Address), Mask: net.CIDRMask(64, 128)}),
|
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipAddress), Mask: net.CIDRMask(24, 32)}),
|
||||||
}
|
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipV6Address), Mask: net.CIDRMask(64, 128)}),
|
||||||
return pi, nil
|
}
|
||||||
})
|
return pi, nil
|
||||||
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Validate that the interface has been updated
|
// Validate that the interface has been updated
|
||||||
|
@@ -11,6 +11,29 @@
|
|||||||
},
|
},
|
||||||
"basePath": "/api/v0",
|
"basePath": "/api/v0",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/audit/entries": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Audit"
|
||||||
|
],
|
||||||
|
"summary": "Get all available audit entries. Ordered by timestamp.",
|
||||||
|
"operationId": "audit_handleEntriesGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.AuditEntry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/login": {
|
"/auth/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -35,7 +58,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/logout": {
|
"/auth/logout": {
|
||||||
"get": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -43,15 +66,12 @@
|
|||||||
"Authentication"
|
"Authentication"
|
||||||
],
|
],
|
||||||
"summary": "Get all available external login providers.",
|
"summary": "Get all available external login providers.",
|
||||||
"operationId": "auth_handleLogoutGet",
|
"operationId": "auth_handleLogoutPost",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"$ref": "#/definitions/model.Error"
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,6 +191,9 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1363,6 +1386,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/user/{id}/interfaces": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Get interfaces for the given user. Returns an empty list if self provisioning is disabled.",
|
||||||
|
"operationId": "users_handleInterfacesGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The user identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.Interface"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/user/{id}/peers": {
|
"/user/{id}/peers": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -1373,6 +1440,15 @@
|
|||||||
],
|
],
|
||||||
"summary": "Get peers for the given user.",
|
"summary": "Get peers for the given user.",
|
||||||
"operationId": "users_handlePeersGet",
|
"operationId": "users_handlePeersGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The user identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -1408,6 +1484,15 @@
|
|||||||
],
|
],
|
||||||
"summary": "Get peer stats for the given user.",
|
"summary": "Get peer stats for the given user.",
|
||||||
"operationId": "users_handleStatsGet",
|
"operationId": "users_handleStatsGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The user identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -1432,6 +1517,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"model.AuditEntry": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ContextUser": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"Message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Origin": {
|
||||||
|
"description": "origin: for example user auth, stats, ...",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Severity": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Timestamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model.ConfigOption-array_string": {
|
"model.ConfigOption-array_string": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1537,6 +1646,10 @@
|
|||||||
"EnabledPeers": {
|
"EnabledPeers": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"Filename": {
|
||||||
|
"description": "the filename of the config file, for example: wg0.conf",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"FirewallMark": {
|
"FirewallMark": {
|
||||||
"description": "a firewall mark",
|
"description": "a firewall mark",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@@ -1778,6 +1891,10 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Filename": {
|
||||||
|
"description": "the filename of the config file, for example: wg_peer_x.conf",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"FirewallMark": {
|
"FirewallMark": {
|
||||||
"description": "a firewall mark",
|
"description": "a firewall mark",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
|
@@ -1,5 +1,21 @@
|
|||||||
basePath: /api/v0
|
basePath: /api/v0
|
||||||
definitions:
|
definitions:
|
||||||
|
model.AuditEntry:
|
||||||
|
properties:
|
||||||
|
ContextUser:
|
||||||
|
type: string
|
||||||
|
Id:
|
||||||
|
type: integer
|
||||||
|
Message:
|
||||||
|
type: string
|
||||||
|
Origin:
|
||||||
|
description: 'origin: for example user auth, stats, ...'
|
||||||
|
type: string
|
||||||
|
Severity:
|
||||||
|
type: string
|
||||||
|
Timestamp:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
model.ConfigOption-array_string:
|
model.ConfigOption-array_string:
|
||||||
properties:
|
properties:
|
||||||
Overridable:
|
Overridable:
|
||||||
@@ -72,6 +88,9 @@ definitions:
|
|||||||
type: array
|
type: array
|
||||||
EnabledPeers:
|
EnabledPeers:
|
||||||
type: integer
|
type: integer
|
||||||
|
Filename:
|
||||||
|
description: 'the filename of the config file, for example: wg0.conf'
|
||||||
|
type: string
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
description: a firewall mark
|
description: a firewall mark
|
||||||
type: integer
|
type: integer
|
||||||
@@ -240,6 +259,9 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
Filename:
|
||||||
|
description: 'the filename of the config file, for example: wg_peer_x.conf'
|
||||||
|
type: string
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.ConfigOption-uint32'
|
- $ref: '#/definitions/model.ConfigOption-uint32'
|
||||||
@@ -419,6 +441,21 @@ info:
|
|||||||
title: WireGuard Portal SPA-UI API
|
title: WireGuard Portal SPA-UI API
|
||||||
version: "0.0"
|
version: "0.0"
|
||||||
paths:
|
paths:
|
||||||
|
/audit/entries:
|
||||||
|
get:
|
||||||
|
operationId: audit_handleEntriesGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.AuditEntry'
|
||||||
|
type: array
|
||||||
|
summary: Get all available audit entries. Ordered by timestamp.
|
||||||
|
tags:
|
||||||
|
- Audit
|
||||||
/auth/{provider}/callback:
|
/auth/{provider}/callback:
|
||||||
get:
|
get:
|
||||||
operationId: auth_handleOauthCallbackGet
|
operationId: auth_handleOauthCallbackGet
|
||||||
@@ -465,17 +502,15 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
/auth/logout:
|
/auth/logout:
|
||||||
get:
|
post:
|
||||||
operationId: auth_handleLogoutGet
|
operationId: auth_handleLogoutPost
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
items:
|
$ref: '#/definitions/model.Error'
|
||||||
$ref: '#/definitions/model.LoginProviderInfo'
|
|
||||||
type: array
|
|
||||||
summary: Get all available external login providers.
|
summary: Get all available external login providers.
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
@@ -523,6 +558,8 @@ paths:
|
|||||||
description: The JavaScript contents
|
description: The JavaScript contents
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
summary: Get the dynamic frontend configuration javascript.
|
summary: Get the dynamic frontend configuration javascript.
|
||||||
tags:
|
tags:
|
||||||
- Configuration
|
- Configuration
|
||||||
@@ -1262,9 +1299,45 @@ paths:
|
|||||||
summary: Enable the REST API for the given user.
|
summary: Enable the REST API for the given user.
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
|
/user/{id}/interfaces:
|
||||||
|
get:
|
||||||
|
operationId: users_handleInterfacesGet
|
||||||
|
parameters:
|
||||||
|
- description: The user identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.Interface'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Get interfaces for the given user. Returns an empty list if self provisioning
|
||||||
|
is disabled.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
/user/{id}/peers:
|
/user/{id}/peers:
|
||||||
get:
|
get:
|
||||||
operationId: users_handlePeersGet
|
operationId: users_handlePeersGet
|
||||||
|
parameters:
|
||||||
|
- description: The user identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -1288,6 +1361,12 @@ paths:
|
|||||||
/user/{id}/stats:
|
/user/{id}/stats:
|
||||||
get:
|
get:
|
||||||
operationId: users_handleStatsGet
|
operationId: users_handleStatsGet
|
||||||
|
parameters:
|
||||||
|
- description: The user identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
@@ -1471,14 +1471,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models.ExpiryDate": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"time.Time": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"models.Interface": {
|
"models.Interface": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -1539,6 +1531,13 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"readOnly": true
|
"readOnly": true
|
||||||
},
|
},
|
||||||
|
"Filename": {
|
||||||
|
"description": "Filename is the name of the config file for this interface.\nThis value is read only and is not settable by the user.",
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 21,
|
||||||
|
"readOnly": true,
|
||||||
|
"example": "wg0.conf"
|
||||||
|
},
|
||||||
"FirewallMark": {
|
"FirewallMark": {
|
||||||
"description": "FirewallMark is an optional firewall mark which is used to handle interface traffic.",
|
"description": "FirewallMark is an optional firewall mark which is used to handle interface traffic.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@@ -1798,11 +1797,7 @@
|
|||||||
},
|
},
|
||||||
"ExpiresAt": {
|
"ExpiresAt": {
|
||||||
"description": "ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. An expired peer is not able to connect.",
|
"description": "ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. An expired peer is not able to connect.",
|
||||||
"allOf": [
|
"type": "string"
|
||||||
{
|
|
||||||
"$ref": "#/definitions/models.ExpiryDate"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"ExtraAllowedIPs": {
|
"ExtraAllowedIPs": {
|
||||||
"description": "ExtraAllowedIPs is a list of additional allowed IP subnets for the peer. These allowed IP subnets are added on the server side.",
|
"description": "ExtraAllowedIPs is a list of additional allowed IP subnets for the peer. These allowed IP subnets are added on the server side.",
|
||||||
@@ -1811,6 +1806,13 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Filename": {
|
||||||
|
"description": "Filename is the name of the config file for this peer.\nThis value is read only and is not settable by the user.",
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 21,
|
||||||
|
"readOnly": true,
|
||||||
|
"example": "wg_peer_x.conf"
|
||||||
|
},
|
||||||
"FirewallMark": {
|
"FirewallMark": {
|
||||||
"description": "FirewallMark is an optional firewall mark which is used to handle peer traffic.",
|
"description": "FirewallMark is an optional firewall mark which is used to handle peer traffic.",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
@@ -1998,8 +2000,7 @@
|
|||||||
"models.User": {
|
"models.User": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"Identifier",
|
"Identifier"
|
||||||
"IsAdmin"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"ApiEnabled": {
|
"ApiEnabled": {
|
||||||
|
@@ -42,11 +42,6 @@ definitions:
|
|||||||
description: Error message.
|
description: Error message.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
models.ExpiryDate:
|
|
||||||
properties:
|
|
||||||
time.Time:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
models.Interface:
|
models.Interface:
|
||||||
properties:
|
properties:
|
||||||
Addresses:
|
Addresses:
|
||||||
@@ -92,6 +87,14 @@ definitions:
|
|||||||
Only enabled peers are able to connect.
|
Only enabled peers are able to connect.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
type: integer
|
type: integer
|
||||||
|
Filename:
|
||||||
|
description: |-
|
||||||
|
Filename is the name of the config file for this interface.
|
||||||
|
This value is read only and is not settable by the user.
|
||||||
|
example: wg0.conf
|
||||||
|
maxLength: 21
|
||||||
|
readOnly: true
|
||||||
|
type: string
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
description: FirewallMark is an optional firewall mark which is used to handle
|
description: FirewallMark is an optional firewall mark which is used to handle
|
||||||
interface traffic.
|
interface traffic.
|
||||||
@@ -306,16 +309,23 @@ definitions:
|
|||||||
- $ref: '#/definitions/models.ConfigOption-string'
|
- $ref: '#/definitions/models.ConfigOption-string'
|
||||||
description: EndpointPublicKey is the endpoint public key.
|
description: EndpointPublicKey is the endpoint public key.
|
||||||
ExpiresAt:
|
ExpiresAt:
|
||||||
allOf:
|
|
||||||
- $ref: '#/definitions/models.ExpiryDate'
|
|
||||||
description: ExpiresAt is the expiry date of the peer in YYYY-MM-DD format.
|
description: ExpiresAt is the expiry date of the peer in YYYY-MM-DD format.
|
||||||
An expired peer is not able to connect.
|
An expired peer is not able to connect.
|
||||||
|
type: string
|
||||||
ExtraAllowedIPs:
|
ExtraAllowedIPs:
|
||||||
description: ExtraAllowedIPs is a list of additional allowed IP subnets for
|
description: ExtraAllowedIPs is a list of additional allowed IP subnets for
|
||||||
the peer. These allowed IP subnets are added on the server side.
|
the peer. These allowed IP subnets are added on the server side.
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
Filename:
|
||||||
|
description: |-
|
||||||
|
Filename is the name of the config file for this peer.
|
||||||
|
This value is read only and is not settable by the user.
|
||||||
|
example: wg_peer_x.conf
|
||||||
|
maxLength: 21
|
||||||
|
readOnly: true
|
||||||
|
type: string
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-uint32'
|
- $ref: '#/definitions/models.ConfigOption-uint32'
|
||||||
@@ -549,7 +559,6 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- Identifier
|
- Identifier
|
||||||
- IsAdmin
|
|
||||||
type: object
|
type: object
|
||||||
models.UserInformation:
|
models.UserInformation:
|
||||||
properties:
|
properties:
|
||||||
|
214
internal/app/api/core/middleware/cors/middleware.go
Normal file
214
internal/app/api/core/middleware/cors/middleware.go
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Middleware is a type that creates a new CORS middleware. The CORS middleware
|
||||||
|
// adds Cross-Origin Resource Sharing headers to the response. This middleware should
|
||||||
|
// be used to allow cross-origin requests to your server.
|
||||||
|
type Middleware struct {
|
||||||
|
o options
|
||||||
|
|
||||||
|
varyHeaders string // precomputed Vary header
|
||||||
|
allOrigins bool // all origins are allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new CORS middleware with the provided options.
|
||||||
|
func New(opts ...Option) *Middleware {
|
||||||
|
o := newOptions(opts...)
|
||||||
|
|
||||||
|
m := &Middleware{
|
||||||
|
o: o,
|
||||||
|
}
|
||||||
|
|
||||||
|
// set vary headers
|
||||||
|
if m.o.allowPrivateNetworks {
|
||||||
|
m.varyHeaders = "Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network"
|
||||||
|
} else {
|
||||||
|
m.varyHeaders = "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.o.allowedOrigins) == 1 && m.o.allowedOrigins[0] == "*" {
|
||||||
|
m.allOrigins = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler returns the CORS middleware handler.
|
||||||
|
func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Handle preflight requests and stop the chain as some other
|
||||||
|
// middleware may not handle OPTIONS requests correctly.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests
|
||||||
|
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
|
||||||
|
m.handlePreflight(w, r)
|
||||||
|
w.WriteHeader(http.StatusNoContent) // always return 204 No Content
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle normal CORS requests
|
||||||
|
m.handleNormal(w, r)
|
||||||
|
next.ServeHTTP(w, r) // execute the next handler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// region internal-helpers
|
||||||
|
|
||||||
|
// handlePreflight handles preflight requests. If the request was successful, this function will
|
||||||
|
// write the CORS headers and return. If the request was not successful, this function will
|
||||||
|
// not add any CORS headers and return - thus the CORS request is considered invalid.
|
||||||
|
func (m *Middleware) handlePreflight(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Always set Vary headers
|
||||||
|
// see https://github.com/rs/cors/issues/10,
|
||||||
|
// https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001
|
||||||
|
w.Header().Add("Vary", m.varyHeaders)
|
||||||
|
|
||||||
|
// check origin
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
if origin == "" {
|
||||||
|
return // not a valid CORS request
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.originAllowed(origin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check method
|
||||||
|
reqMethod := r.Header.Get("Access-Control-Request-Method")
|
||||||
|
if !m.methodAllowed(reqMethod) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check headers
|
||||||
|
reqHeaders := r.Header.Get("Access-Control-Request-Headers")
|
||||||
|
if !m.headersAllowed(reqHeaders) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// set CORS headers for the successful preflight request
|
||||||
|
if m.allOrigins {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", origin) // return original origin
|
||||||
|
}
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", reqMethod)
|
||||||
|
if reqHeaders != "" {
|
||||||
|
// Spec says: Since the list of headers can be unbounded, simply returning supported headers
|
||||||
|
// from Access-Control-Request-Headers can be enough
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", reqHeaders)
|
||||||
|
}
|
||||||
|
if m.o.allowCredentials {
|
||||||
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
if m.o.allowPrivateNetworks && r.Header.Get("Access-Control-Request-Private-Network") == "true" {
|
||||||
|
w.Header().Set("Access-Control-Allow-Private-Network", "true")
|
||||||
|
}
|
||||||
|
if m.o.maxAge > 0 {
|
||||||
|
w.Header().Set("Access-Control-Max-Age", strconv.Itoa(m.o.maxAge))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNormal handles normal CORS requests. If the request was successful, this function will
|
||||||
|
// write the CORS headers to the response. If the request was not successful, this function will
|
||||||
|
// not add any CORS headers to the response. In this case, the CORS request is considered invalid.
|
||||||
|
func (m *Middleware) handleNormal(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Always set Vary headers
|
||||||
|
// see https://github.com/rs/cors/issues/10,
|
||||||
|
// https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001
|
||||||
|
w.Header().Add("Vary", "Origin")
|
||||||
|
|
||||||
|
// check origin
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
if origin == "" {
|
||||||
|
return // not a valid CORS request
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.originAllowed(origin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check method
|
||||||
|
if !m.methodAllowed(r.Method) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// set CORS headers for the successful CORS request
|
||||||
|
if m.allOrigins {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", origin) // return original origin
|
||||||
|
}
|
||||||
|
if len(m.o.exposedHeaders) > 0 {
|
||||||
|
w.Header().Set("Access-Control-Expose-Headers", strings.Join(m.o.exposedHeaders, ", "))
|
||||||
|
}
|
||||||
|
if m.o.allowCredentials {
|
||||||
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) originAllowed(origin string) bool {
|
||||||
|
if len(m.o.allowedOrigins) == 1 && m.o.allowedOrigins[0] == "*" {
|
||||||
|
return true // everything is allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// check simple origins
|
||||||
|
if slices.Contains(m.o.allowedOrigins, origin) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check wildcard origins
|
||||||
|
for _, allowedOrigin := range m.o.allowedOriginPatterns {
|
||||||
|
if allowedOrigin.match(origin) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) methodAllowed(method string) bool {
|
||||||
|
if method == http.MethodOptions {
|
||||||
|
return true // preflight request is always allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.o.allowedMethods) == 1 && m.o.allowedMethods[0] == "*" {
|
||||||
|
return true // everything is allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(m.o.allowedMethods, method) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) headersAllowed(headers string) bool {
|
||||||
|
if headers == "" {
|
||||||
|
return true // no headers are requested
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.o.allowedHeaders) == 0 {
|
||||||
|
return false // no headers are allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := m.o.allowedHeaders["*"]; ok {
|
||||||
|
return true // everything is allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// split headers by comma (according to definition, the headers are sorted and in lowercase)
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers
|
||||||
|
for header := range strings.SplitSeq(headers, ",") {
|
||||||
|
if _, ok := m.o.allowedHeaders[strings.TrimSpace(header)]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion internal-helpers
|
101
internal/app/api/core/middleware/cors/middleware_test.go
Normal file
101
internal/app/api/core/middleware/cors/middleware_test.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMiddleware_New(t *testing.T) {
|
||||||
|
m := New(WithAllowedOrigins("*"))
|
||||||
|
|
||||||
|
if len(m.varyHeaders) == 0 {
|
||||||
|
t.Errorf("expected vary headers to be populated, got %v", m.varyHeaders)
|
||||||
|
}
|
||||||
|
if !m.allOrigins {
|
||||||
|
t.Errorf("expected allOrigins to be true, got %v", m.allOrigins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_normal(t *testing.T) {
|
||||||
|
m := New(WithAllowedOrigins("http://example.com"))
|
||||||
|
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||||
|
req.Header.Set("Origin", "http://example.com")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Result().StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("expected status code 200, got %d", w.Result().StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.Header().Get("Access-Control-Allow-Origin") != "http://example.com" {
|
||||||
|
t.Errorf("expected Access-Control-Allow-Origin to be 'http://example.com', got %s",
|
||||||
|
w.Header().Get("Access-Control-Allow-Origin"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_preflight(t *testing.T) {
|
||||||
|
m := New(WithAllowedOrigins("http://example.com"))
|
||||||
|
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodOptions, "http://example.com", nil)
|
||||||
|
req.Header.Set("Origin", "http://example.com")
|
||||||
|
req.Header.Set("Access-Control-Request-Method", http.MethodGet)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Result().StatusCode != http.StatusNoContent {
|
||||||
|
t.Errorf("expected status code 204, got %d", w.Result().StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.Header().Get("Access-Control-Allow-Origin") != "http://example.com" {
|
||||||
|
t.Errorf("expected Access-Control-Allow-Origin to be 'http://example.com', got %s",
|
||||||
|
w.Header().Get("Access-Control-Allow-Origin"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_originAllowed(t *testing.T) {
|
||||||
|
m := New(WithAllowedOrigins("http://example.com"))
|
||||||
|
|
||||||
|
if !m.originAllowed("http://example.com") {
|
||||||
|
t.Errorf("expected origin 'http://example.com' to be allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.originAllowed("http://notallowed.com") {
|
||||||
|
t.Errorf("expected origin 'http://notallowed.com' to be not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_methodAllowed(t *testing.T) {
|
||||||
|
m := New(WithAllowedMethods(http.MethodGet, http.MethodPost))
|
||||||
|
|
||||||
|
if !m.methodAllowed(http.MethodGet) {
|
||||||
|
t.Errorf("expected method 'GET' to be allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.methodAllowed(http.MethodDelete) {
|
||||||
|
t.Errorf("expected method 'DELETE' to be not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_headersAllowed(t *testing.T) {
|
||||||
|
m := New(WithAllowedHeaders("Content-Type", "Authorization"))
|
||||||
|
|
||||||
|
if !m.headersAllowed("content-type, authorization") {
|
||||||
|
t.Errorf("expected headers 'Content-Type, Authorization' to be allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.headersAllowed("x-custom-header") {
|
||||||
|
t.Errorf("expected header 'X-Custom-Header' to be not allowed")
|
||||||
|
}
|
||||||
|
}
|
133
internal/app/api/core/middleware/cors/options.go
Normal file
133
internal/app/api/core/middleware/cors/options.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type void struct{}
|
||||||
|
|
||||||
|
// options is a struct that contains options for the CORS middleware.
|
||||||
|
// It uses the functional options pattern for flexible configuration.
|
||||||
|
type options struct {
|
||||||
|
allowedOrigins []string // origins without wildcards
|
||||||
|
allowedOriginPatterns []wildcard // origins with wildcards
|
||||||
|
allowedMethods []string
|
||||||
|
allowedHeaders map[string]void
|
||||||
|
exposedHeaders []string // these are in addition to the CORS-safelisted response headers
|
||||||
|
allowCredentials bool
|
||||||
|
allowPrivateNetworks bool
|
||||||
|
maxAge int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a type that is used to set options for the CORS middleware.
|
||||||
|
// It implements the functional options pattern.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
// WithAllowedOrigins sets the allowed origins for the CORS middleware.
|
||||||
|
// If the special "*" value is present in the list, all origins will be allowed.
|
||||||
|
// An origin may contain a wildcard (*) to replace 0 or more characters
|
||||||
|
// (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty.
|
||||||
|
// Only one wildcard can be used per origin.
|
||||||
|
// By default, all origins are allowed (*).
|
||||||
|
func WithAllowedOrigins(origins ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.allowedOrigins = nil
|
||||||
|
o.allowedOriginPatterns = nil
|
||||||
|
|
||||||
|
for _, origin := range origins {
|
||||||
|
if len(origin) > 1 && strings.Contains(origin, "*") {
|
||||||
|
o.allowedOriginPatterns = append(
|
||||||
|
o.allowedOriginPatterns,
|
||||||
|
newWildcard(origin),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
o.allowedOrigins = append(o.allowedOrigins, origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllowedMethods sets the allowed methods for the CORS middleware.
|
||||||
|
// By default, all methods are allowed (*).
|
||||||
|
func WithAllowedMethods(methods ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.allowedMethods = methods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllowedHeaders sets the allowed headers for the CORS middleware.
|
||||||
|
// By default, all headers are allowed (*).
|
||||||
|
func WithAllowedHeaders(headers ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.allowedHeaders = make(map[string]void)
|
||||||
|
|
||||||
|
for _, header := range headers {
|
||||||
|
// allowed headers are always checked in lowercase
|
||||||
|
o.allowedHeaders[strings.ToLower(header)] = void{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExposedHeaders sets the exposed headers for the CORS middleware.
|
||||||
|
// By default, no headers are exposed.
|
||||||
|
func WithExposedHeaders(headers ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.exposedHeaders = nil
|
||||||
|
|
||||||
|
for _, header := range headers {
|
||||||
|
o.exposedHeaders = append(o.exposedHeaders, http.CanonicalHeaderKey(header))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllowCredentials sets the allow credentials option for the CORS middleware.
|
||||||
|
// This setting indicates whether the request can include user credentials like
|
||||||
|
// cookies, HTTP authentication or client side SSL certificates.
|
||||||
|
// By default, credentials are not allowed.
|
||||||
|
func WithAllowCredentials(allow bool) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.allowCredentials = allow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllowPrivateNetworks sets the allow private networks option for the CORS middleware.
|
||||||
|
// This setting indicates whether to accept cross-origin requests over a private network.
|
||||||
|
func WithAllowPrivateNetworks(allow bool) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.allowPrivateNetworks = allow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMaxAge sets the max age (in seconds) for the CORS middleware.
|
||||||
|
// The maximum age indicates how long (in seconds) the results of a preflight request
|
||||||
|
// can be cached. A value of 0 means that no Access-Control-Max-Age header is sent back,
|
||||||
|
// resulting in browsers using their default value (5s by spec).
|
||||||
|
// If you need to force a 0 max-age, set it to a negative value (ie: -1).
|
||||||
|
// By default, the max age is 7200 seconds.
|
||||||
|
func WithMaxAge(age int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.maxAge = age
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newOptions is a function that returns a new options struct with sane default values.
|
||||||
|
func newOptions(opts ...Option) options {
|
||||||
|
o := options{
|
||||||
|
allowedOrigins: []string{"*"},
|
||||||
|
allowedMethods: []string{
|
||||||
|
http.MethodHead, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete,
|
||||||
|
},
|
||||||
|
allowedHeaders: map[string]void{"*": {}},
|
||||||
|
exposedHeaders: nil,
|
||||||
|
allowCredentials: false,
|
||||||
|
allowPrivateNetworks: false,
|
||||||
|
maxAge: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
}
|
96
internal/app/api/core/middleware/cors/options_test.go
Normal file
96
internal/app/api/core/middleware/cors/options_test.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWithAllowedOrigins(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
origins []string
|
||||||
|
wantNormal []string
|
||||||
|
wantWildcard []wildcard
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No origins",
|
||||||
|
origins: []string{},
|
||||||
|
wantNormal: nil,
|
||||||
|
wantWildcard: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single origin",
|
||||||
|
origins: []string{"http://example.com"},
|
||||||
|
wantNormal: []string{"http://example.com"},
|
||||||
|
wantWildcard: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wildcard origin",
|
||||||
|
origins: []string{"http://*.example.com"},
|
||||||
|
wantNormal: nil,
|
||||||
|
wantWildcard: []wildcard{newWildcard("http://*.example.com")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
o := newOptions(WithAllowedOrigins(tt.origins...))
|
||||||
|
if !slices.Equal(o.allowedOrigins, tt.wantNormal) {
|
||||||
|
t.Errorf("got %v, want %v", o, tt.wantNormal)
|
||||||
|
}
|
||||||
|
if !slices.Equal(o.allowedOriginPatterns, tt.wantWildcard) {
|
||||||
|
t.Errorf("got %v, want %v", o, tt.wantWildcard)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithAllowedMethods(t *testing.T) {
|
||||||
|
methods := []string{http.MethodGet, http.MethodPost}
|
||||||
|
o := newOptions(WithAllowedMethods(methods...))
|
||||||
|
if !slices.Equal(o.allowedMethods, methods) {
|
||||||
|
t.Errorf("got %v, want %v", o.allowedMethods, methods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithAllowedHeaders(t *testing.T) {
|
||||||
|
headers := []string{"Content-Type", "Authorization"}
|
||||||
|
o := newOptions(WithAllowedHeaders(headers...))
|
||||||
|
expectedHeaders := map[string]void{"content-type": {}, "authorization": {}}
|
||||||
|
if !maps.Equal(o.allowedHeaders, expectedHeaders) {
|
||||||
|
t.Errorf("got %v, want %v", o.allowedHeaders, expectedHeaders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithExposedHeaders(t *testing.T) {
|
||||||
|
headers := []string{"X-Custom-Header"}
|
||||||
|
o := newOptions(WithExposedHeaders(headers...))
|
||||||
|
expectedHeaders := []string{http.CanonicalHeaderKey("X-Custom-Header")}
|
||||||
|
if !slices.Equal(o.exposedHeaders, expectedHeaders) {
|
||||||
|
t.Errorf("got %v, want %v", o.exposedHeaders, expectedHeaders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithAllowCredentials(t *testing.T) {
|
||||||
|
o := newOptions(WithAllowCredentials(true))
|
||||||
|
if !o.allowCredentials {
|
||||||
|
t.Errorf("got %v, want %v", o.allowCredentials, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithAllowPrivateNetworks(t *testing.T) {
|
||||||
|
o := newOptions(WithAllowPrivateNetworks(true))
|
||||||
|
if !o.allowPrivateNetworks {
|
||||||
|
t.Errorf("got %v, want %v", o.allowPrivateNetworks, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithMaxAge(t *testing.T) {
|
||||||
|
maxAge := 3600
|
||||||
|
o := newOptions(WithMaxAge(maxAge))
|
||||||
|
if o.maxAge != maxAge {
|
||||||
|
t.Errorf("got %v, want %v", o.maxAge, maxAge)
|
||||||
|
}
|
||||||
|
}
|
33
internal/app/api/core/middleware/cors/wildcard.go
Normal file
33
internal/app/api/core/middleware/cors/wildcard.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// wildcard is a type that represents a wildcard string.
|
||||||
|
// This type allows faster matching of strings with a wildcard
|
||||||
|
// in comparison to using regex.
|
||||||
|
type wildcard struct {
|
||||||
|
prefix string
|
||||||
|
suffix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// match returns true if the string s has the prefix and suffix of the wildcard.
|
||||||
|
func (w wildcard) match(s string) bool {
|
||||||
|
return len(s) >= len(w.prefix)+len(w.suffix) &&
|
||||||
|
strings.HasPrefix(s, w.prefix) &&
|
||||||
|
strings.HasSuffix(s, w.suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWildcard(s string) wildcard {
|
||||||
|
if i := strings.IndexByte(s, '*'); i >= 0 {
|
||||||
|
return wildcard{
|
||||||
|
prefix: s[:i],
|
||||||
|
suffix: s[i+1:],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback, usually this case should not happen
|
||||||
|
return wildcard{
|
||||||
|
prefix: s,
|
||||||
|
suffix: "",
|
||||||
|
}
|
||||||
|
}
|
94
internal/app/api/core/middleware/cors/wildcard_test.go
Normal file
94
internal/app/api/core/middleware/cors/wildcard_test.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestWildcardMatch(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
wildcard wildcard
|
||||||
|
input string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Match with prefix and suffix",
|
||||||
|
wildcard: newWildcard("http://*.example.com"),
|
||||||
|
input: "http://sub.example.com",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No match with different prefix",
|
||||||
|
wildcard: newWildcard("http://*.example.com"),
|
||||||
|
input: "https://sub.example.com",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No match with different suffix",
|
||||||
|
wildcard: newWildcard("http://*.example.com"),
|
||||||
|
input: "http://sub.example.org",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Match with empty suffix",
|
||||||
|
wildcard: newWildcard("http://*"),
|
||||||
|
input: "http://example.com",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Match with empty prefix",
|
||||||
|
wildcard: newWildcard("*.example.com"),
|
||||||
|
input: "sub.example.com",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No match with empty prefix and different suffix",
|
||||||
|
wildcard: newWildcard("*.example.com"),
|
||||||
|
input: "sub.example.org",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := tt.wildcard.match(tt.input); got != tt.expected {
|
||||||
|
t.Errorf("wildcard.match(%s) = %v, want %v", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWildcard(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected wildcard
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Wildcard with prefix and suffix",
|
||||||
|
input: "http://*.example.com",
|
||||||
|
expected: wildcard{prefix: "http://", suffix: ".example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wildcard with empty suffix",
|
||||||
|
input: "http://*",
|
||||||
|
expected: wildcard{prefix: "http://", suffix: ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wildcard with empty prefix",
|
||||||
|
input: "*.example.com",
|
||||||
|
expected: wildcard{prefix: "", suffix: ".example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No wildcard character",
|
||||||
|
input: "http://example.com",
|
||||||
|
expected: wildcard{prefix: "http://example.com", suffix: ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := newWildcard(tt.input); got != tt.expected {
|
||||||
|
t.Errorf("newWildcard(%s) = %v, want %v", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
137
internal/app/api/core/middleware/csrf/middleware.go
Normal file
137
internal/app/api/core/middleware/csrf/middleware.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContextValueIdentifier is the context value identifier for the CSRF token.
|
||||||
|
// The token is only stored in the context if the RefreshToken function was called before.
|
||||||
|
const ContextValueIdentifier = "_csrf_token"
|
||||||
|
|
||||||
|
// Middleware is a type that creates a new CSRF middleware. The CSRF middleware
|
||||||
|
// can be used to mitigate Cross-Site Request Forgery attacks.
|
||||||
|
type Middleware struct {
|
||||||
|
o options
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new CSRF middleware with the provided options.
|
||||||
|
func New(sessionReader SessionReader, sessionWriter SessionWriter, opts ...Option) *Middleware {
|
||||||
|
opts = append(opts, withSessionReader(sessionReader), withSessionWriter(sessionWriter))
|
||||||
|
o := newOptions(opts...)
|
||||||
|
|
||||||
|
m := &Middleware{
|
||||||
|
o: o,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForPRNG()
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler returns the CSRF middleware handler. This middleware validates the CSRF token and calls the specified
|
||||||
|
// error handler if an invalid CSRF token was found.
|
||||||
|
func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if slices.Contains(m.o.ignoreMethods, r.Method) {
|
||||||
|
next.ServeHTTP(w, r) // skip CSRF check for ignored methods
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the token from the request
|
||||||
|
token := m.o.tokenGetter(r)
|
||||||
|
storedToken := m.o.sessionGetter(r)
|
||||||
|
|
||||||
|
if !tokenEqual(token, storedToken) {
|
||||||
|
m.o.errCallback(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r) // execute the next handler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken generates a new CSRF Token and stores it in the session. The token is also passed to subsequent handlers
|
||||||
|
// via the context value ContextValueIdentifier.
|
||||||
|
func (m *Middleware) RefreshToken(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if GetToken(r.Context()) != "" {
|
||||||
|
// token already generated higher up in the chain
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate a new token
|
||||||
|
token := generateToken(m.o.tokenLength)
|
||||||
|
key := generateToken(m.o.tokenLength)
|
||||||
|
|
||||||
|
// mask the token
|
||||||
|
maskedToken := maskToken(token, key)
|
||||||
|
encodedToken := encodeToken(maskedToken)
|
||||||
|
|
||||||
|
// pass the token down the chain via the context
|
||||||
|
r = r.WithContext(setToken(r.Context(), encodedToken))
|
||||||
|
|
||||||
|
// store the token in the session
|
||||||
|
m.o.sessionWriter(r, encodedToken)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// region token-access
|
||||||
|
|
||||||
|
// GetToken retrieves the CSRF token from the given context. Ensure that the RefreshToken function was called before,
|
||||||
|
// otherwise, no token is populated in the context.
|
||||||
|
func GetToken(ctx context.Context) string {
|
||||||
|
token, ok := ctx.Value(ContextValueIdentifier).(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion token-access
|
||||||
|
|
||||||
|
// region internal-helpers
|
||||||
|
|
||||||
|
func setToken(ctx context.Context, token string) context.Context {
|
||||||
|
return context.WithValue(ctx, ContextValueIdentifier, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultTokenGetter is the default token getter function for the CSRF middleware.
|
||||||
|
// It checks the request form values, URL query parameters, and headers for the CSRF token.
|
||||||
|
// The order of precedence is:
|
||||||
|
// 1. Header "X-CSRF-TOKEN"
|
||||||
|
// 2. Header "X-XSRF-TOKEN"
|
||||||
|
// 3. URL query parameter "_csrf"
|
||||||
|
// 4. Form value "_csrf"
|
||||||
|
func defaultTokenGetter(r *http.Request) string {
|
||||||
|
if t := r.Header.Get("X-CSRF-TOKEN"); len(t) > 0 {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if t := r.Header.Get("X-XSRF-TOKEN"); len(t) > 0 {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if t := r.URL.Query().Get("_csrf"); len(t) > 0 {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if t := r.FormValue("_csrf"); len(t) > 0 {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultErrorHandler is the default error handler function for the CSRF middleware.
|
||||||
|
// It writes a 403 Forbidden response.
|
||||||
|
func defaultErrorHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
http.Error(w, "CSRF token mismatch", http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion internal-helpers
|
251
internal/app/api/core/middleware/csrf/middleware_test.go
Normal file
251
internal/app/api/core/middleware/csrf/middleware_test.go
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/core/request"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMiddleware_Handler(t *testing.T) {
|
||||||
|
sessionToken := "stored-token"
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
token string
|
||||||
|
wantStatus int
|
||||||
|
}{
|
||||||
|
{"ValidToken", "POST", "stored-token", http.StatusOK},
|
||||||
|
{"ValidToken2", "PUT", "stored-token", http.StatusOK},
|
||||||
|
{"ValidToken3", "GET", "stored-token", http.StatusOK},
|
||||||
|
{"InvalidToken", "POST", "invalid-token", http.StatusForbidden},
|
||||||
|
{"IgnoredMethod", "GET", "", http.StatusOK},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(tt.method, "/", nil)
|
||||||
|
req.Header.Set("X-CSRF-TOKEN", tt.token)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != tt.wantStatus {
|
||||||
|
t.Errorf("Handler() status = %d, want %d", status, tt.wantStatus)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_RefreshToken(t *testing.T) {
|
||||||
|
sessionToken := ""
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
handler := m.RefreshToken(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := GetToken(r.Context())
|
||||||
|
if token == "" {
|
||||||
|
t.Errorf("RefreshToken() did not set token in context")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("RefreshToken() status = %d, want %d", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionToken == "" {
|
||||||
|
t.Errorf("RefreshToken() did not set token in session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_RefreshToken_chained(t *testing.T) {
|
||||||
|
sessionToken := ""
|
||||||
|
tokenWrites := 0
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
tokenWrites++
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
handler := m.RefreshToken(m.RefreshToken(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := GetToken(r.Context())
|
||||||
|
if token == "" {
|
||||||
|
t.Errorf("RefreshToken() did not set token in context")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("RefreshToken() status = %d, want %d", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionToken == "" {
|
||||||
|
t.Errorf("RefreshToken() did not set token in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenWrites != 1 {
|
||||||
|
t.Errorf("RefreshToken() wrote token to session more than once: %d", tokenWrites)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_RefreshToken_Handler(t *testing.T) {
|
||||||
|
sessionToken := ""
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
// simulate two requests: first one GET request with the RefreshToken handler, the next one is a PUT request with
|
||||||
|
// the token from the first request added as X-CSRF-TOKEN header
|
||||||
|
|
||||||
|
// first request
|
||||||
|
retrievedToken := ""
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := m.RefreshToken(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
retrievedToken = GetToken(r.Context())
|
||||||
|
if retrievedToken == "" {
|
||||||
|
t.Errorf("RefreshToken() did not set token in context")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
}))
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
if status := rr.Code; status != http.StatusAccepted {
|
||||||
|
t.Errorf("Handler() status = %d, want %d", status, http.StatusAccepted)
|
||||||
|
}
|
||||||
|
if retrievedToken == "" {
|
||||||
|
t.Errorf("no token retrieved")
|
||||||
|
}
|
||||||
|
if retrievedToken != sessionToken {
|
||||||
|
t.Errorf("token in context does not match token in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
// second request
|
||||||
|
req = httptest.NewRequest("PUT", "/", nil)
|
||||||
|
req.Header.Set("X-CSRF-TOKEN", retrievedToken)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
handler = m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("Handler() status = %d, want %d", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_FormBody(t *testing.T) {
|
||||||
|
sessionToken := "stored-token"
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bodyData, err := request.BodyString(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Handler() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
// ensure that the body is empty - ParseForm() should have been called before by the CSRF middleware
|
||||||
|
if bodyData != "" {
|
||||||
|
t.Errorf("Handler() bodyData = %s, want empty", bodyData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.FormValue("_csrf") != "stored-token" {
|
||||||
|
t.Errorf("Handler() _csrf = %s, want %s", r.FormValue("_csrf"), "stored-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/", nil)
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Form = make(map[string][]string)
|
||||||
|
req.Form.Add("_csrf", "stored-token")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("Handler() status = %d, want %d", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_FormBodyAvailable(t *testing.T) {
|
||||||
|
sessionToken := "stored-token"
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bodyData, err := request.BodyString(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Handler() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
// ensure that the body is not empty, as the CSRF middleware should not have read the body
|
||||||
|
if bodyData != "the original body" {
|
||||||
|
t.Errorf("Handler() bodyData = %s, want %s", bodyData, "the original body")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the token is available in the form values (from query parameters)
|
||||||
|
if r.FormValue("_csrf") != "stored-token" {
|
||||||
|
t.Errorf("Handler() _csrf = %s, want %s", r.FormValue("_csrf"), "stored-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/?_csrf=stored-token", nil)
|
||||||
|
req.Header.Set("Content-Type", "text/plain")
|
||||||
|
req.Body = io.NopCloser(strings.NewReader("the original body"))
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("Handler() status = %d, want %d", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
88
internal/app/api/core/middleware/csrf/options.go
Normal file
88
internal/app/api/core/middleware/csrf/options.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type SessionReader func(r *http.Request) string
|
||||||
|
type SessionWriter func(r *http.Request, token string)
|
||||||
|
|
||||||
|
// options is a struct that contains options for the CSRF middleware.
|
||||||
|
// It uses the functional options pattern for flexible configuration.
|
||||||
|
type options struct {
|
||||||
|
tokenLength int
|
||||||
|
ignoreMethods []string
|
||||||
|
|
||||||
|
errCallbackOverride bool
|
||||||
|
errCallback func(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
|
tokenGetterOverride bool
|
||||||
|
tokenGetter func(r *http.Request) string
|
||||||
|
|
||||||
|
sessionGetter SessionReader
|
||||||
|
sessionWriter SessionWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a type that is used to set options for the CSRF middleware.
|
||||||
|
// It implements the functional options pattern.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
// WithTokenLength is a method that sets the token length for the CSRF middleware.
|
||||||
|
// The default value is 32.
|
||||||
|
func WithTokenLength(length int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.tokenLength = length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithErrorCallback is a method that sets the error callback function for the CSRF middleware.
|
||||||
|
// The error callback function is called when the CSRF token is invalid.
|
||||||
|
// The default behavior is to write a 403 Forbidden response.
|
||||||
|
func WithErrorCallback(fn func(w http.ResponseWriter, r *http.Request)) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.errCallback = fn
|
||||||
|
o.errCallbackOverride = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTokenGetter is a method that sets the token getter function for the CSRF middleware.
|
||||||
|
// The token getter function is called to get the CSRF token from the request.
|
||||||
|
// The default behavior is to get the token from the "X-CSRF-Token" header.
|
||||||
|
func WithTokenGetter(fn func(r *http.Request) string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.tokenGetter = fn
|
||||||
|
o.tokenGetterOverride = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// withSessionReader is a method that sets the session reader function for the CSRF middleware.
|
||||||
|
// The session reader function is called to get the CSRF token from the session.
|
||||||
|
func withSessionReader(fn SessionReader) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.sessionGetter = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// withSessionWriter is a method that sets the session writer function for the CSRF middleware.
|
||||||
|
// The session writer function is called to write the CSRF token to the session.
|
||||||
|
func withSessionWriter(fn SessionWriter) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.sessionWriter = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newOptions is a function that returns a new options struct with sane default values.
|
||||||
|
func newOptions(opts ...Option) options {
|
||||||
|
o := options{
|
||||||
|
tokenLength: 32,
|
||||||
|
ignoreMethods: []string{"GET", "HEAD", "OPTIONS"},
|
||||||
|
errCallbackOverride: false,
|
||||||
|
errCallback: defaultErrorHandler,
|
||||||
|
tokenGetterOverride: false,
|
||||||
|
tokenGetter: defaultTokenGetter,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
}
|
75
internal/app/api/core/middleware/csrf/options_test.go
Normal file
75
internal/app/api/core/middleware/csrf/options_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWithTokenLength(t *testing.T) {
|
||||||
|
o := newOptions(WithTokenLength(64))
|
||||||
|
if o.tokenLength != 64 {
|
||||||
|
t.Errorf("WithTokenLength() = %d, want %d", o.tokenLength, 64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithErrorCallback(t *testing.T) {
|
||||||
|
callback := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
}
|
||||||
|
o := newOptions(WithErrorCallback(callback))
|
||||||
|
if !o.errCallbackOverride {
|
||||||
|
t.Errorf("WithErrorCallback() did not set errCallbackOverride to true")
|
||||||
|
}
|
||||||
|
if o.errCallback == nil {
|
||||||
|
t.Errorf("WithErrorCallback() did not set errCallback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithTokenGetter(t *testing.T) {
|
||||||
|
getter := func(r *http.Request) string {
|
||||||
|
return "test-token"
|
||||||
|
}
|
||||||
|
o := newOptions(WithTokenGetter(getter))
|
||||||
|
if !o.tokenGetterOverride {
|
||||||
|
t.Errorf("WithTokenGetter() did not set tokenGetterOverride to true")
|
||||||
|
}
|
||||||
|
if o.tokenGetter == nil {
|
||||||
|
t.Errorf("WithTokenGetter() did not set tokenGetter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithSessionReader(t *testing.T) {
|
||||||
|
reader := func(r *http.Request) string {
|
||||||
|
return "session-token"
|
||||||
|
}
|
||||||
|
o := newOptions(withSessionReader(reader))
|
||||||
|
if o.sessionGetter == nil {
|
||||||
|
t.Errorf("withSessionReader() did not set sessionGetter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithSessionWriter(t *testing.T) {
|
||||||
|
writer := func(r *http.Request, token string) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
o := newOptions(withSessionWriter(writer))
|
||||||
|
if o.sessionWriter == nil {
|
||||||
|
t.Errorf("withSessionWriter() did not set sessionWriter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewOptionsDefaults(t *testing.T) {
|
||||||
|
o := newOptions()
|
||||||
|
if o.tokenLength != 32 {
|
||||||
|
t.Errorf("newOptions() default tokenLength = %d, want %d", o.tokenLength, 32)
|
||||||
|
}
|
||||||
|
if len(o.ignoreMethods) != 3 {
|
||||||
|
t.Errorf("newOptions() default ignoreMethods length = %d, want %d", len(o.ignoreMethods), 3)
|
||||||
|
}
|
||||||
|
if o.errCallback == nil {
|
||||||
|
t.Errorf("newOptions() default errCallback is nil")
|
||||||
|
}
|
||||||
|
if o.tokenGetter == nil {
|
||||||
|
t.Errorf("newOptions() default tokenGetter is nil")
|
||||||
|
}
|
||||||
|
}
|
90
internal/app/api/core/middleware/csrf/token.go
Normal file
90
internal/app/api/core/middleware/csrf/token.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkForPRNG is a function that checks if a cryptographically secure PRNG is available.
|
||||||
|
// If it is not available, the function panics.
|
||||||
|
func checkForPRNG() {
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
_, err := io.ReadFull(rand.Reader, buf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("crypto/rand is unavailable: Read() failed with %#v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateToken is a function that generates a secure random CSRF token.
|
||||||
|
func generateToken(length int) []byte {
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
|
||||||
|
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeToken is a function that encodes a token to a base64 string.
|
||||||
|
func encodeToken(token []byte) string {
|
||||||
|
return base64.URLEncoding.EncodeToString(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeToken is a function that decodes a base64 string to a token.
|
||||||
|
func decodeToken(token string) ([]byte, error) {
|
||||||
|
return base64.URLEncoding.DecodeString(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// maskToken is a function that masks a token with a given key.
|
||||||
|
// The returned byte slice contains the key + the masked token.
|
||||||
|
// The key needs to have the same length as the token, otherwise the function panics.
|
||||||
|
// So the resulting slice has a length of len(token) * 2.
|
||||||
|
func maskToken(token, key []byte) []byte {
|
||||||
|
if len(token) != len(key) {
|
||||||
|
panic("token and key must have the same length")
|
||||||
|
}
|
||||||
|
|
||||||
|
// masked contains the key in the first half and the XOR masked token in the second half
|
||||||
|
tokenLength := len(token)
|
||||||
|
masked := make([]byte, tokenLength*2)
|
||||||
|
for i := 0; i < len(token); i++ {
|
||||||
|
masked[i] = key[i]
|
||||||
|
masked[i+tokenLength] = token[i] ^ key[i] // XOR mask
|
||||||
|
}
|
||||||
|
|
||||||
|
return masked
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmaskToken is a function that unmask a token which contains the key in the first half.
|
||||||
|
// The returned byte slice contains the unmasked token, it has exactly half the length of the input slice.
|
||||||
|
func unmaskToken(masked []byte) []byte {
|
||||||
|
tokenLength := len(masked) / 2
|
||||||
|
token := make([]byte, tokenLength)
|
||||||
|
for i := 0; i < tokenLength; i++ {
|
||||||
|
token[i] = masked[i] ^ masked[i+tokenLength] // XOR unmask
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenEqual is a function that compares two tokens for equality.
|
||||||
|
func tokenEqual(a, b string) bool {
|
||||||
|
decodedA, err := decodeToken(a)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
decodedB, err := decodeToken(b)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
unmaskedA := unmaskToken(decodedA)
|
||||||
|
unmaskedB := unmaskToken(decodedB)
|
||||||
|
|
||||||
|
return slices.Equal(unmaskedA, unmaskedB)
|
||||||
|
}
|
81
internal/app/api/core/middleware/csrf/token_test.go
Normal file
81
internal/app/api/core/middleware/csrf/token_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckForPRNG(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("checkForPRNG() panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
checkForPRNG()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateToken(t *testing.T) {
|
||||||
|
length := 32
|
||||||
|
token := generateToken(length)
|
||||||
|
if len(token) != length {
|
||||||
|
t.Errorf("generateToken() returned token of length %d, expected %d", len(token), length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeToken(t *testing.T) {
|
||||||
|
token := []byte("testtoken")
|
||||||
|
encoded := encodeToken(token)
|
||||||
|
expected := base64.URLEncoding.EncodeToString(token)
|
||||||
|
if encoded != expected {
|
||||||
|
t.Errorf("encodeToken() = %v, want %v", encoded, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeToken(t *testing.T) {
|
||||||
|
token := "dGVzdHRva2Vu"
|
||||||
|
expected := []byte("testtoken")
|
||||||
|
decoded, err := decodeToken(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("decodeToken() error = %v", err)
|
||||||
|
}
|
||||||
|
if string(decoded) != string(expected) {
|
||||||
|
t.Errorf("decodeToken() = %v, want %v", decoded, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaskToken(t *testing.T) {
|
||||||
|
token := []byte("testtoken")
|
||||||
|
key := []byte("keykeykey")
|
||||||
|
masked := maskToken(token, key)
|
||||||
|
if len(masked) != len(token)*2 {
|
||||||
|
t.Errorf("maskToken() returned masked token of length %d, expected %d", len(masked), len(token)*2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmaskToken(t *testing.T) {
|
||||||
|
token := []byte("testtoken")
|
||||||
|
key := []byte("keykeykey")
|
||||||
|
masked := maskToken(token, key)
|
||||||
|
unmasked := unmaskToken(masked)
|
||||||
|
if string(unmasked) != string(token) {
|
||||||
|
t.Errorf("unmaskToken() = %v, want %v", unmasked, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenEqual(t *testing.T) {
|
||||||
|
tokenA := encodeToken(maskToken([]byte{0x01, 0x02, 0x03}, []byte{0x01, 0x02, 0x03}))
|
||||||
|
tokenB := encodeToken(maskToken([]byte{0x01, 0x02, 0x03}, []byte{0x04, 0x05, 0x06}))
|
||||||
|
if !tokenEqual(tokenA, tokenB) {
|
||||||
|
t.Errorf("tokenEqual() = false, want true")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenC := encodeToken(maskToken([]byte{0x01, 0x02, 0x03}, []byte{0x07, 0x08, 0x09}))
|
||||||
|
if !tokenEqual(tokenA, tokenC) {
|
||||||
|
t.Errorf("tokenEqual() = false, want true")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenD := encodeToken(maskToken([]byte{0x09, 0x02, 0x03}, []byte{0x04, 0x05, 0x06}))
|
||||||
|
if tokenEqual(tokenA, tokenD) {
|
||||||
|
t.Errorf("tokenEqual() = true, want false")
|
||||||
|
}
|
||||||
|
}
|
199
internal/app/api/core/middleware/logging/middleware.go
Normal file
199
internal/app/api/core/middleware/logging/middleware.go
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogLevel is an enumeration of the different log levels.
|
||||||
|
type LogLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogLevelDebug LogLevel = iota
|
||||||
|
LogLevelInfo
|
||||||
|
LogLevelWarn
|
||||||
|
LogLevelError
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is an interface that defines the methods that a logger must implement.
|
||||||
|
// This allows the logging middleware to be used with different logging libraries.
|
||||||
|
type Logger interface {
|
||||||
|
// Debugf logs a message at debug level.
|
||||||
|
Debugf(format string, args ...any)
|
||||||
|
// Infof logs a message at info level.
|
||||||
|
Infof(format string, args ...any)
|
||||||
|
// Warnf logs a message at warn level.
|
||||||
|
Warnf(format string, args ...any)
|
||||||
|
// Errorf logs a message at error level.
|
||||||
|
Errorf(format string, args ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware is a type that creates a new logging middleware. The logging middleware
|
||||||
|
// logs information about each request.
|
||||||
|
type Middleware struct {
|
||||||
|
o options
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new logging middleware with the provided options.
|
||||||
|
func New(opts ...Option) *Middleware {
|
||||||
|
o := newOptions(opts...)
|
||||||
|
|
||||||
|
m := &Middleware{
|
||||||
|
o: o,
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler returns the logging middleware handler.
|
||||||
|
func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ww := newWriterWrapper(w)
|
||||||
|
start := time.Now()
|
||||||
|
defer func() {
|
||||||
|
info := m.extractInfoMap(r, start, ww)
|
||||||
|
|
||||||
|
if m.o.logger == nil {
|
||||||
|
msg, args := m.buildSlogMessageAndArguments(info)
|
||||||
|
m.logMsg(msg, args...)
|
||||||
|
} else {
|
||||||
|
msg := m.buildNormalLogMessage(info)
|
||||||
|
m.logMsg(msg)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
next.ServeHTTP(ww, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) extractInfoMap(r *http.Request, start time.Time, ww *writerWrapper) map[string]any {
|
||||||
|
info := make(map[string]any)
|
||||||
|
|
||||||
|
info["method"] = r.Method
|
||||||
|
info["path"] = r.URL.Path
|
||||||
|
info["protocol"] = r.Proto
|
||||||
|
info["clientIP"] = r.Header.Get("X-Forwarded-For")
|
||||||
|
if info["clientIP"] == "" {
|
||||||
|
// If the X-Forwarded-For header is not set, use the remote address without the port number.
|
||||||
|
lastColonIndex := strings.LastIndex(r.RemoteAddr, ":")
|
||||||
|
switch lastColonIndex {
|
||||||
|
case -1:
|
||||||
|
info["clientIP"] = r.RemoteAddr
|
||||||
|
default:
|
||||||
|
info["clientIP"] = r.RemoteAddr[:lastColonIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info["userAgent"] = r.UserAgent()
|
||||||
|
info["referer"] = r.Header.Get("Referer")
|
||||||
|
info["duration"] = time.Since(start).String()
|
||||||
|
info["status"] = ww.StatusCode
|
||||||
|
info["dataLength"] = ww.WrittenBytes
|
||||||
|
|
||||||
|
if m.o.headerRequestIdKey != "" {
|
||||||
|
info["headerRequestId"] = r.Header.Get(m.o.headerRequestIdKey)
|
||||||
|
}
|
||||||
|
if m.o.contextRequestIdKey != "" {
|
||||||
|
info["contextRequestId"], _ = r.Context().Value(m.o.contextRequestIdKey).(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) buildNormalLogMessage(info map[string]any) string {
|
||||||
|
switch {
|
||||||
|
case info["headerRequestId"] != nil && info["contextRequestId"] != nil:
|
||||||
|
return fmt.Sprintf("%s %s %s - %d %d - %s - %s %s %s - rid=%s ctx=%s",
|
||||||
|
info["method"], info["path"], info["protocol"],
|
||||||
|
info["status"], info["dataLength"],
|
||||||
|
info["duration"],
|
||||||
|
info["clientIP"], info["userAgent"], info["referer"],
|
||||||
|
info["headerRequestId"], info["contextRequestId"])
|
||||||
|
case info["headerRequestId"] != nil:
|
||||||
|
return fmt.Sprintf("%s %s %s - %d %d - %s - %s %s %s - rid=%s",
|
||||||
|
info["method"], info["path"], info["protocol"],
|
||||||
|
info["status"], info["dataLength"],
|
||||||
|
info["duration"],
|
||||||
|
info["clientIP"], info["userAgent"], info["referer"],
|
||||||
|
info["headerRequestId"])
|
||||||
|
case info["contextRequestId"] != nil:
|
||||||
|
return fmt.Sprintf("%s %s %s - %d %d - %s - %s %s %s - ctx=%s",
|
||||||
|
info["method"], info["path"], info["protocol"],
|
||||||
|
info["status"], info["dataLength"],
|
||||||
|
info["duration"],
|
||||||
|
info["clientIP"], info["userAgent"], info["referer"],
|
||||||
|
info["contextRequestId"])
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s %s %s - %d %d - %s - %s %s %s",
|
||||||
|
info["method"], info["path"], info["protocol"],
|
||||||
|
info["status"], info["dataLength"],
|
||||||
|
info["duration"],
|
||||||
|
info["clientIP"], info["userAgent"], info["referer"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) buildSlogMessageAndArguments(info map[string]any) (message string, args []any) {
|
||||||
|
message = fmt.Sprintf("%s %s", info["method"], info["path"])
|
||||||
|
|
||||||
|
// Use a fixed order for the keys, so that the message is always the same.
|
||||||
|
// Skip method and path as they are already in the message.
|
||||||
|
keys := []string{
|
||||||
|
"protocol",
|
||||||
|
"status",
|
||||||
|
"dataLength",
|
||||||
|
"duration",
|
||||||
|
"clientIP",
|
||||||
|
"userAgent",
|
||||||
|
"referer",
|
||||||
|
"headerRequestId",
|
||||||
|
"contextRequestId",
|
||||||
|
}
|
||||||
|
for _, k := range keys {
|
||||||
|
if v, ok := info[k]; ok {
|
||||||
|
args = append(args, k, v) // only add key, value if it exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) addPrefix(message string) string {
|
||||||
|
if m.o.prefix != "" {
|
||||||
|
return m.o.prefix + " " + message
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) logMsg(message string, args ...any) {
|
||||||
|
message = m.addPrefix(message)
|
||||||
|
|
||||||
|
if m.o.logger != nil {
|
||||||
|
switch m.o.logLevel {
|
||||||
|
case LogLevelDebug:
|
||||||
|
m.o.logger.Debugf(message, args...)
|
||||||
|
case LogLevelInfo:
|
||||||
|
m.o.logger.Infof(message, args...)
|
||||||
|
case LogLevelWarn:
|
||||||
|
m.o.logger.Warnf(message, args...)
|
||||||
|
case LogLevelError:
|
||||||
|
m.o.logger.Errorf(message, args...)
|
||||||
|
default:
|
||||||
|
m.o.logger.Infof(message, args...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch m.o.logLevel {
|
||||||
|
case LogLevelDebug:
|
||||||
|
slog.Debug(message, args...)
|
||||||
|
case LogLevelInfo:
|
||||||
|
slog.Info(message, args...)
|
||||||
|
case LogLevelWarn:
|
||||||
|
slog.Warn(message, args...)
|
||||||
|
case LogLevelError:
|
||||||
|
slog.Error(message, args...)
|
||||||
|
default:
|
||||||
|
slog.Info(message, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
148
internal/app/api/core/middleware/logging/middleware_test.go
Normal file
148
internal/app/api/core/middleware/logging/middleware_test.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockLogger struct {
|
||||||
|
messages []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockLogger) Debugf(format string, _ ...any) {
|
||||||
|
m.messages = append(m.messages, "DEBUG: "+format)
|
||||||
|
}
|
||||||
|
func (m *mockLogger) Infof(format string, _ ...any) {
|
||||||
|
m.messages = append(m.messages, "INFO: "+format)
|
||||||
|
}
|
||||||
|
func (m *mockLogger) Warnf(format string, _ ...any) {
|
||||||
|
m.messages = append(m.messages, "WARN: "+format)
|
||||||
|
}
|
||||||
|
func (m *mockLogger) Errorf(format string, _ ...any) {
|
||||||
|
m.messages = append(m.messages, "ERROR: "+format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Normal(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithLogger(logger), WithLevel(LogLevelError)).Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusTeapot {
|
||||||
|
t.Errorf("expected status code to be %v, got %v", http.StatusTeapot, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "Hello, World!"
|
||||||
|
if rr.Body.String() != expected {
|
||||||
|
t.Errorf("expected response body to be %v, got %v", expected, rr.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(logger.messages) == 0 {
|
||||||
|
t.Errorf("expected log messages, got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(logger.messages) != 0 && !strings.Contains(logger.messages[0], "ERROR: GET /foo") {
|
||||||
|
t.Errorf("expected log message to contain request info, got %v", logger.messages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Extended(t *testing.T) {
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithContextRequestIdKey("requestId"), WithHeaderRequestIdKey("X-Request-Id")).
|
||||||
|
Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusTeapot {
|
||||||
|
t.Errorf("expected status code to be %v, got %v", http.StatusTeapot, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "Hello, World!"
|
||||||
|
if rr.Body.String() != expected {
|
||||||
|
t.Errorf("expected response body to be %v, got %v", expected, rr.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Logger_remoteAddr(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithLogger(logger), WithLevel(LogLevelError)).Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
req.RemoteAddr = "xhamster.com:1234"
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Logger_remoteAddrNoPort(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithLogger(logger), WithLevel(LogLevelError)).Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
req.RemoteAddr = "xhamster.com"
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Logger_remoteAddrV6(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithLogger(logger), WithLevel(LogLevelError)).Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
req.RemoteAddr = "[::1]:4711"
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Logger_remoteAddrV6NoPort(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithLogger(logger), WithLevel(LogLevelError)).Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
req.RemoteAddr = "[::1]"
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
}
|
80
internal/app/api/core/middleware/logging/options.go
Normal file
80
internal/app/api/core/middleware/logging/options.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
// options is a struct that contains options for the logging middleware.
|
||||||
|
// It uses the functional options pattern for flexible configuration.
|
||||||
|
type options struct {
|
||||||
|
logLevel LogLevel
|
||||||
|
logger Logger
|
||||||
|
prefix string
|
||||||
|
|
||||||
|
contextRequestIdKey string
|
||||||
|
headerRequestIdKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a type that is used to set options for the logging middleware.
|
||||||
|
// It implements the functional options pattern.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
// WithLevel is a method that sets the log level for the logging middleware.
|
||||||
|
// Possible values are LogLevelDebug, LogLevelInfo, LogLevelWarn, and LogLevelError.
|
||||||
|
// The default value is LogLevelInfo.
|
||||||
|
func WithLevel(level LogLevel) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.logLevel = level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPrefix is a method that sets the prefix for the logging middleware.
|
||||||
|
// If a prefix is set, it will be prepended to each log message. A space will
|
||||||
|
// be added between the prefix and the log message.
|
||||||
|
// The default value is an empty string.
|
||||||
|
func WithPrefix(prefix string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.prefix = prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContextRequestIdKey is a method that sets the key for the request ID in the
|
||||||
|
// request context. If a key is set, the logging middleware will use this key to
|
||||||
|
// retrieve the request ID from the request context.
|
||||||
|
// The default value is an empty string, meaning the request ID will not be logged.
|
||||||
|
func WithContextRequestIdKey(key string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.contextRequestIdKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeaderRequestIdKey is a method that sets the key for the request ID in the
|
||||||
|
// request headers. If a key is set, the logging middleware will use this key to
|
||||||
|
// retrieve the request ID from the request headers.
|
||||||
|
// The default value is an empty string, meaning the request ID will not be logged.
|
||||||
|
func WithHeaderRequestIdKey(key string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.headerRequestIdKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogger is a method that sets the logger for the logging middleware.
|
||||||
|
// If a logger is set, the logging middleware will use this logger to log messages.
|
||||||
|
// The default logger is the structured slog logger.
|
||||||
|
func WithLogger(logger Logger) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.logger = logger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newOptions is a function that returns a new options struct with sane default values.
|
||||||
|
func newOptions(opts ...Option) options {
|
||||||
|
o := options{
|
||||||
|
logLevel: LogLevelInfo,
|
||||||
|
logger: nil,
|
||||||
|
prefix: "",
|
||||||
|
contextRequestIdKey: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
}
|
88
internal/app/api/core/middleware/logging/options_test.go
Normal file
88
internal/app/api/core/middleware/logging/options_test.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWithLevel(t *testing.T) {
|
||||||
|
// table test to check all possible log levels
|
||||||
|
levels := []LogLevel{
|
||||||
|
LogLevelDebug,
|
||||||
|
LogLevelInfo,
|
||||||
|
LogLevelWarn,
|
||||||
|
LogLevelError,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, level := range levels {
|
||||||
|
opt := WithLevel(level)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.logLevel != level {
|
||||||
|
t.Errorf("expected log level to be %v, got %v", level, o.logLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithPrefix(t *testing.T) {
|
||||||
|
prefix := "TEST"
|
||||||
|
opt := WithPrefix(prefix)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.prefix != prefix {
|
||||||
|
t.Errorf("expected prefix to be %v, got %v", prefix, o.prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithContextRequestIdKey(t *testing.T) {
|
||||||
|
key := "contextKey"
|
||||||
|
opt := WithContextRequestIdKey(key)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.contextRequestIdKey != key {
|
||||||
|
t.Errorf("expected contextRequestIdKey to be %v, got %v", key, o.contextRequestIdKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithHeaderRequestIdKey(t *testing.T) {
|
||||||
|
key := "headerKey"
|
||||||
|
opt := WithHeaderRequestIdKey(key)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.headerRequestIdKey != key {
|
||||||
|
t.Errorf("expected headerRequestIdKey to be %v, got %v", key, o.headerRequestIdKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithLogger(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
opt := WithLogger(logger)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.logger != logger {
|
||||||
|
t.Errorf("expected logger to be %v, got %v", logger, o.logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaults(t *testing.T) {
|
||||||
|
o := newOptions()
|
||||||
|
|
||||||
|
if o.logLevel != LogLevelInfo {
|
||||||
|
t.Errorf("expected log level to be %v, got %v", LogLevelInfo, o.logLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.logger != nil {
|
||||||
|
t.Errorf("expected logger to be nil, got %v", o.logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.prefix != "" {
|
||||||
|
t.Errorf("expected prefix to be empty, got %v", o.prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.contextRequestIdKey != "" {
|
||||||
|
t.Errorf("expected contextRequestIdKey to be empty, got %v", o.contextRequestIdKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.headerRequestIdKey != "" {
|
||||||
|
t.Errorf("expected headerRequestIdKey to be empty, got %v", o.headerRequestIdKey)
|
||||||
|
}
|
||||||
|
}
|
45
internal/app/api/core/middleware/logging/writer.go
Normal file
45
internal/app/api/core/middleware/logging/writer.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// writerWrapper wraps a http.ResponseWriter and tracks the number of bytes written to it.
|
||||||
|
// It also tracks the http response code passed to the WriteHeader func of
|
||||||
|
// the ResponseWriter.
|
||||||
|
type writerWrapper struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
|
||||||
|
// StatusCode is the last http response code passed to the WriteHeader func of
|
||||||
|
// the ResponseWriter. If no such call is made, a default code of http.StatusOK
|
||||||
|
// is assumed instead.
|
||||||
|
StatusCode int
|
||||||
|
|
||||||
|
// WrittenBytes is the number of bytes successfully written by the Write or
|
||||||
|
// ReadFrom function of the ResponseWriter. ResponseWriters may also write
|
||||||
|
// data to their underlaying connection directly (e.g. headers), but those
|
||||||
|
// are not tracked. Therefor the number of Written bytes will usually match
|
||||||
|
// the size of the response body.
|
||||||
|
WrittenBytes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader wraps the WriteHeader method of the ResponseWriter and tracks the
|
||||||
|
// http response code passed to it.
|
||||||
|
func (w *writerWrapper) WriteHeader(code int) {
|
||||||
|
w.StatusCode = code
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write wraps the Write method of the ResponseWriter and tracks the number of bytes
|
||||||
|
// written to it.
|
||||||
|
func (w *writerWrapper) Write(data []byte) (int, error) {
|
||||||
|
n, err := w.ResponseWriter.Write(data)
|
||||||
|
w.WrittenBytes += int64(n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// newWriterWrapper returns a new writerWrapper that wraps the given http.ResponseWriter.
|
||||||
|
// It initializes the StatusCode to http.StatusOK.
|
||||||
|
func newWriterWrapper(w http.ResponseWriter) *writerWrapper {
|
||||||
|
return &writerWrapper{ResponseWriter: w, StatusCode: http.StatusOK}
|
||||||
|
}
|
85
internal/app/api/core/middleware/logging/writer_test.go
Normal file
85
internal/app/api/core/middleware/logging/writer_test.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriterWrapper_WriteHeader(t *testing.T) {
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ww := newWriterWrapper(rr)
|
||||||
|
|
||||||
|
ww.WriteHeader(http.StatusNotFound)
|
||||||
|
|
||||||
|
if ww.StatusCode != http.StatusNotFound {
|
||||||
|
t.Errorf("expected status code to be %v, got %v", http.StatusNotFound, ww.StatusCode)
|
||||||
|
}
|
||||||
|
if rr.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("expected recorder status code to be %v, got %v", http.StatusNotFound, rr.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriterWrapper_Write(t *testing.T) {
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ww := newWriterWrapper(rr)
|
||||||
|
|
||||||
|
data := []byte("Hello, World!")
|
||||||
|
n, err := ww.Write(data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if n != len(data) {
|
||||||
|
t.Errorf("expected written bytes to be %v, got %v", len(data), n)
|
||||||
|
}
|
||||||
|
if ww.WrittenBytes != int64(len(data)) {
|
||||||
|
t.Errorf("expected WrittenBytes to be %v, got %v", len(data), ww.WrittenBytes)
|
||||||
|
}
|
||||||
|
if rr.Body.String() != string(data) {
|
||||||
|
t.Errorf("expected response body to be %v, got %v", string(data), rr.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriterWrapper_WriteWithHeaders(t *testing.T) {
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ww := newWriterWrapper(rr)
|
||||||
|
|
||||||
|
data := []byte("Hello, World!")
|
||||||
|
n, err := ww.Write(data)
|
||||||
|
|
||||||
|
ww.Header().Set("Content-Type", "text/plain")
|
||||||
|
ww.Header().Set("X-Some-Header", "some-value")
|
||||||
|
ww.WriteHeader(http.StatusTeapot)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if n != len(data) {
|
||||||
|
t.Errorf("expected written bytes to be %v, got %v", len(data), n)
|
||||||
|
}
|
||||||
|
if ww.WrittenBytes != int64(len(data)) {
|
||||||
|
t.Errorf("expected WrittenBytes to be %v, got %v", len(data), ww.WrittenBytes)
|
||||||
|
}
|
||||||
|
if rr.Body.String() != string(data) {
|
||||||
|
t.Errorf("expected response body to be %v, got %v", string(data), rr.Body.String())
|
||||||
|
}
|
||||||
|
if ww.StatusCode != http.StatusTeapot {
|
||||||
|
t.Errorf("expected status code to be %v, got %v", http.StatusTeapot, ww.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWriterWrapper(t *testing.T) {
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
ww := newWriterWrapper(rr)
|
||||||
|
|
||||||
|
if ww.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("expected initial status code to be %v, got %v", http.StatusOK, ww.StatusCode)
|
||||||
|
}
|
||||||
|
if ww.WrittenBytes != 0 {
|
||||||
|
t.Errorf("expected initial WrittenBytes to be %v, got %v", 0, ww.WrittenBytes)
|
||||||
|
}
|
||||||
|
if ww.ResponseWriter != rr {
|
||||||
|
t.Errorf("expected ResponseWriter to be %v, got %v", rr, ww.ResponseWriter)
|
||||||
|
}
|
||||||
|
}
|
133
internal/app/api/core/middleware/recovery/middleware.go
Normal file
133
internal/app/api/core/middleware/recovery/middleware.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package recovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is an interface that defines the methods that a logger must implement.
|
||||||
|
// This allows the logging middleware to be used with different logging libraries.
|
||||||
|
type Logger interface {
|
||||||
|
// Errorf logs a message at error level.
|
||||||
|
Errorf(format string, args ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware is a type that creates a new recovery middleware. The recovery middleware
|
||||||
|
// recovers from panics and returns an Internal Server Error response. This middleware should
|
||||||
|
// be the first middleware in the middleware chain, so that it can recover from panics in other
|
||||||
|
// middlewares.
|
||||||
|
type Middleware struct {
|
||||||
|
o options
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new recovery middleware with the provided options.
|
||||||
|
func New(opts ...Option) *Middleware {
|
||||||
|
o := newOptions(opts...)
|
||||||
|
|
||||||
|
m := &Middleware{
|
||||||
|
o: o,
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler returns the recovery middleware handler.
|
||||||
|
func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
stack := debug.Stack()
|
||||||
|
|
||||||
|
realErr, ok := err.(error)
|
||||||
|
if !ok {
|
||||||
|
realErr = fmt.Errorf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a broken connection, as it is not really a
|
||||||
|
// condition that warrants a panic stack trace.
|
||||||
|
brokenPipe := isBrokenPipeError(realErr)
|
||||||
|
|
||||||
|
// Log the error and stack trace
|
||||||
|
if m.o.logCallback != nil {
|
||||||
|
m.o.logCallback(realErr, stack, brokenPipe)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case brokenPipe && m.o.brokenPipeCallback != nil:
|
||||||
|
m.o.brokenPipeCallback(realErr, stack, w, r)
|
||||||
|
case !brokenPipe && m.o.errCallback != nil:
|
||||||
|
m.o.errCallback(realErr, stack, w, r)
|
||||||
|
default:
|
||||||
|
// no callback set, simply recover and do nothing...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPrefix(o options, message string) string {
|
||||||
|
if o.defaultLogPrefix != "" {
|
||||||
|
return o.defaultLogPrefix + " " + message
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultErrCallback is the default error callback function for the recovery middleware.
|
||||||
|
// It writes a JSON response with an Internal Server Error status code. If the exposeStackTrace option is
|
||||||
|
// enabled, the stack trace is included in the response.
|
||||||
|
func getDefaultErrCallback(o options) func(err error, stack []byte, w http.ResponseWriter, r *http.Request) {
|
||||||
|
return func(err error, stack []byte, w http.ResponseWriter, r *http.Request) {
|
||||||
|
responseBody := map[string]interface{}{
|
||||||
|
"error": "Internal Server Error",
|
||||||
|
}
|
||||||
|
if o.exposeStackTrace && len(stack) > 0 {
|
||||||
|
responseBody["stack"] = string(stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBody, _ := json.Marshal(responseBody)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = w.Write(jsonBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultLogCallback is the default log callback function for the recovery middleware.
|
||||||
|
// It logs the error and stack trace using the structured slog logger or the provided logger in Error level.
|
||||||
|
func getDefaultLogCallback(o options) func(error, []byte, bool) {
|
||||||
|
return func(err error, stack []byte, brokenPipe bool) {
|
||||||
|
if brokenPipe {
|
||||||
|
return // by default, ignore broken pipe errors
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case o.useSlog:
|
||||||
|
slog.Error(addPrefix(o, err.Error()), "stack", string(stack))
|
||||||
|
case o.logger != nil:
|
||||||
|
o.logger.Errorf(fmt.Sprintf("%s; stacktrace=%s", addPrefix(o, err.Error()), string(stack)))
|
||||||
|
default:
|
||||||
|
// no logger set, do nothing...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBrokenPipeError(err error) bool {
|
||||||
|
var syscallErr *os.SyscallError
|
||||||
|
if errors.As(err, &syscallErr) {
|
||||||
|
errMsg := strings.ToLower(syscallErr.Err.Error())
|
||||||
|
if strings.Contains(errMsg, "broken pipe") ||
|
||||||
|
strings.Contains(errMsg, "connection reset by peer") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
149
internal/app/api/core/middleware/recovery/middleware_test.go
Normal file
149
internal/app/api/core/middleware/recovery/middleware_test.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package recovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockLogger struct{}
|
||||||
|
|
||||||
|
func (m *mockLogger) Errorf(_ string, _ ...any) {}
|
||||||
|
|
||||||
|
func TestMiddleware(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
options []Option
|
||||||
|
panicSimulator func()
|
||||||
|
expectedStatus int
|
||||||
|
expectedBody string
|
||||||
|
expectStack bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default behavior",
|
||||||
|
options: []Option{},
|
||||||
|
panicSimulator: func() {
|
||||||
|
panic(errors.New("test panic"))
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusInternalServerError,
|
||||||
|
expectedBody: `{"error":"Internal Server Error"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom error callback",
|
||||||
|
options: []Option{
|
||||||
|
WithErrCallback(func(err error, stack []byte, w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
w.Write([]byte("custom error"))
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
panicSimulator: func() {
|
||||||
|
panic(errors.New("test panic"))
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusTeapot,
|
||||||
|
expectedBody: "custom error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "broken pipe error",
|
||||||
|
options: []Option{
|
||||||
|
WithBrokenPipeCallback(func(err error, stack []byte, w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
w.Write([]byte("broken pipe"))
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
panicSimulator: func() {
|
||||||
|
panic(&os.SyscallError{Err: errors.New("broken pipe")})
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusServiceUnavailable,
|
||||||
|
expectedBody: "broken pipe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default callback broken pipe error",
|
||||||
|
options: nil,
|
||||||
|
panicSimulator: func() {
|
||||||
|
panic(&os.SyscallError{Err: errors.New("broken pipe")})
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedBody: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default callback normal error",
|
||||||
|
options: nil,
|
||||||
|
panicSimulator: func() {
|
||||||
|
panic("something went wrong")
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusInternalServerError,
|
||||||
|
expectedBody: "{\"error\":\"Internal Server Error\"}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default callback with stack trace",
|
||||||
|
options: []Option{
|
||||||
|
WithExposeStackTrace(true),
|
||||||
|
},
|
||||||
|
panicSimulator: func() {
|
||||||
|
panic("something went wrong")
|
||||||
|
},
|
||||||
|
expectedStatus: http.StatusInternalServerError,
|
||||||
|
expectedBody: "\"stack\":",
|
||||||
|
expectStack: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
handler := New(tt.options...).Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tt.panicSimulator()
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if rr.Code != tt.expectedStatus {
|
||||||
|
t.Errorf("expected status %v, got %v", tt.expectedStatus, rr.Code)
|
||||||
|
}
|
||||||
|
if !tt.expectStack && rr.Body.String() != tt.expectedBody {
|
||||||
|
t.Errorf("expected body %v, got %v", tt.expectedBody, rr.Body.String())
|
||||||
|
}
|
||||||
|
if tt.expectStack && !strings.Contains(rr.Body.String(), tt.expectedBody) {
|
||||||
|
t.Errorf("expected body to contain %v, got %v", tt.expectedBody, rr.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsBrokenPipeError(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "broken pipe error",
|
||||||
|
err: &os.SyscallError{Err: errors.New("broken pipe")},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "connection reset by peer error",
|
||||||
|
err: &os.SyscallError{Err: errors.New("connection reset by peer")},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other error",
|
||||||
|
err: errors.New("other error"),
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := isBrokenPipeError(tt.err)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
129
internal/app/api/core/middleware/recovery/options.go
Normal file
129
internal/app/api/core/middleware/recovery/options.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package recovery
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// options is a struct that contains options for the recovery middleware.
|
||||||
|
// It uses the functional options pattern for flexible configuration.
|
||||||
|
type options struct {
|
||||||
|
logger Logger
|
||||||
|
useSlog bool
|
||||||
|
|
||||||
|
errCallbackOverride bool
|
||||||
|
errCallback func(err error, stack []byte, w http.ResponseWriter, r *http.Request)
|
||||||
|
brokenPipeCallbackOverride bool
|
||||||
|
brokenPipeCallback func(err error, stack []byte, w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
|
exposeStackTrace bool
|
||||||
|
defaultLogPrefix string
|
||||||
|
logCallbackOverride bool
|
||||||
|
logCallback func(err error, stack []byte, brokenPipe bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a type that is used to set options for the recovery middleware.
|
||||||
|
// It implements the functional options pattern.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
// WithErrCallback sets the error callback function for the recovery middleware.
|
||||||
|
// The error callback function is called when a panic is recovered by the middleware.
|
||||||
|
// This function completely overrides the default behavior of the middleware. It is the
|
||||||
|
// responsibility of the user to handle the error and write a response to the client.
|
||||||
|
//
|
||||||
|
// Ensure that this function does not panic, as it will be called in a deferred function!
|
||||||
|
func WithErrCallback(fn func(err error, stack []byte, w http.ResponseWriter, r *http.Request)) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.errCallback = fn
|
||||||
|
o.errCallbackOverride = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBrokenPipeCallback sets the broken pipe callback function for the recovery middleware.
|
||||||
|
// The broken pipe callback function is called when a broken pipe error is recovered by the middleware.
|
||||||
|
// This function completely overrides the default behavior of the middleware. It is the responsibility
|
||||||
|
// of the user to handle the error and write a response to the client.
|
||||||
|
//
|
||||||
|
// Ensure that this function does not panic, as it will be called in a deferred function!
|
||||||
|
func WithBrokenPipeCallback(fn func(err error, stack []byte, w http.ResponseWriter, r *http.Request)) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.brokenPipeCallback = fn
|
||||||
|
o.brokenPipeCallbackOverride = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogCallback sets the log callback function for the recovery middleware.
|
||||||
|
// The log callback function is called when a panic is recovered by the middleware.
|
||||||
|
// This function allows the user to log the error and stack trace. The default behavior is to log
|
||||||
|
// the error and stack trace in Error level.
|
||||||
|
// This function completely overrides the default behavior of the middleware.
|
||||||
|
//
|
||||||
|
// Ensure that this function does not panic, as it will be called in a deferred function!
|
||||||
|
func WithLogCallback(fn func(err error, stack []byte, brokenPipe bool)) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.logCallback = fn
|
||||||
|
o.logCallbackOverride = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogger is a method that sets the logger for the logging middleware.
|
||||||
|
// If a logger is set, the logging middleware will use this logger to log messages.
|
||||||
|
// The default logger is the structured slog logger, see WithSlog.
|
||||||
|
func WithLogger(logger Logger) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.logger = logger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSlog is a method that sets whether the recovery middleware should use the structured slog logger.
|
||||||
|
// If set to true, the middleware will use the structured slog logger. If set to false, the middleware
|
||||||
|
// will not use any logger unless one is explicitly set with the WithLogger option.
|
||||||
|
// The default value is true.
|
||||||
|
func WithSlog(useSlog bool) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.useSlog = useSlog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDefaultLogPrefix is a method that sets the default log prefix for the recovery middleware.
|
||||||
|
// If a default log prefix is set and the default log callback is used, the prefix will be prepended
|
||||||
|
// to each log message. A space will be added between the prefix and the log message.
|
||||||
|
// The default value is an empty string.
|
||||||
|
func WithDefaultLogPrefix(defaultLogPrefix string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.defaultLogPrefix = defaultLogPrefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExposeStackTrace is a method that sets whether the stack trace should be exposed in the response.
|
||||||
|
// If set to true, the stack trace will be included in the response body. If set to false, the stack trace
|
||||||
|
// will not be included in the response body. This only applies to the default error callback.
|
||||||
|
// The default value is false.
|
||||||
|
func WithExposeStackTrace(exposeStackTrace bool) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.exposeStackTrace = exposeStackTrace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newOptions is a function that returns a new options struct with sane default values.
|
||||||
|
func newOptions(opts ...Option) options {
|
||||||
|
o := options{
|
||||||
|
logger: nil,
|
||||||
|
useSlog: true,
|
||||||
|
errCallback: nil,
|
||||||
|
brokenPipeCallback: nil, // by default, ignore broken pipe errors
|
||||||
|
exposeStackTrace: false,
|
||||||
|
defaultLogPrefix: "",
|
||||||
|
logCallback: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.errCallback == nil && !o.errCallbackOverride {
|
||||||
|
o.errCallback = getDefaultErrCallback(o)
|
||||||
|
}
|
||||||
|
if o.logCallback == nil && !o.logCallbackOverride {
|
||||||
|
o.logCallback = getDefaultLogCallback(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
}
|
100
internal/app/api/core/middleware/recovery/options_test.go
Normal file
100
internal/app/api/core/middleware/recovery/options_test.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package recovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWithErrCallback(t *testing.T) {
|
||||||
|
callback := func(err error, stack []byte, w http.ResponseWriter, r *http.Request) {}
|
||||||
|
opt := WithErrCallback(callback)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.errCallback == nil {
|
||||||
|
t.Errorf("expected errCallback to be set, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithBrokenPipeCallback(t *testing.T) {
|
||||||
|
callback := func(err error, stack []byte, w http.ResponseWriter, r *http.Request) {}
|
||||||
|
opt := WithBrokenPipeCallback(callback)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.brokenPipeCallback == nil {
|
||||||
|
t.Errorf("expected brokenPipeCallback to be set, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithLogCallback(t *testing.T) {
|
||||||
|
callback := func(err error, stack []byte, brokenPipe bool) {}
|
||||||
|
opt := WithLogCallback(callback)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.logCallback == nil {
|
||||||
|
t.Errorf("expected logCallback to be set, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithLogger(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
opt := WithLogger(logger)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.logger != logger {
|
||||||
|
t.Errorf("expected logger to be %v, got %v", logger, o.logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithSlog(t *testing.T) {
|
||||||
|
opt := WithSlog(false)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.useSlog != false {
|
||||||
|
t.Errorf("expected useSlog to be false, got %v", o.useSlog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithDefaultLogPrefix(t *testing.T) {
|
||||||
|
prefix := "PREFIX"
|
||||||
|
opt := WithDefaultLogPrefix(prefix)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.defaultLogPrefix != prefix {
|
||||||
|
t.Errorf("expected defaultLogPrefix to be %v, got %v", prefix, o.defaultLogPrefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithExposeStackTrace(t *testing.T) {
|
||||||
|
opt := WithExposeStackTrace(true)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.exposeStackTrace != true {
|
||||||
|
t.Errorf("expected exposeStackTrace to be true, got %v", o.exposeStackTrace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewOptionsDefaults(t *testing.T) {
|
||||||
|
o := newOptions()
|
||||||
|
|
||||||
|
if o.logger != nil {
|
||||||
|
t.Errorf("expected logger to be nil, got %v", o.logger)
|
||||||
|
}
|
||||||
|
if o.useSlog != true {
|
||||||
|
t.Errorf("expected useSlog to be true, got %v", o.useSlog)
|
||||||
|
}
|
||||||
|
if o.errCallback == nil {
|
||||||
|
t.Errorf("expected errCallback to be set, got nil")
|
||||||
|
}
|
||||||
|
if o.brokenPipeCallback != nil {
|
||||||
|
t.Errorf("expected brokenPipeCallback to be nil, got %T", o.brokenPipeCallback)
|
||||||
|
}
|
||||||
|
if o.exposeStackTrace != false {
|
||||||
|
t.Errorf("expected exposeStackTrace to be false, got %T", o.exposeStackTrace)
|
||||||
|
}
|
||||||
|
if o.defaultLogPrefix != "" {
|
||||||
|
t.Errorf("expected defaultLogPrefix to be empty, got %T", o.defaultLogPrefix)
|
||||||
|
}
|
||||||
|
if o.logCallback == nil {
|
||||||
|
t.Errorf("expected logCallback to be set, got nil")
|
||||||
|
}
|
||||||
|
}
|
69
internal/app/api/core/middleware/tracing/middleware.go
Normal file
69
internal/app/api/core/middleware/tracing/middleware.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package tracing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Middleware is a type that creates a new tracing middleware. The tracing middleware
|
||||||
|
// can be used to trace requests based on a request ID header or parameter.
|
||||||
|
type Middleware struct {
|
||||||
|
o options
|
||||||
|
|
||||||
|
seededRand *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new CORS middleware with the provided options.
|
||||||
|
func New(opts ...Option) *Middleware {
|
||||||
|
o := newOptions(opts...)
|
||||||
|
|
||||||
|
m := &Middleware{
|
||||||
|
o: o,
|
||||||
|
seededRand: rand.New(rand.NewSource(o.generateSeed)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler returns the tracing middleware handler.
|
||||||
|
func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var reqId string
|
||||||
|
|
||||||
|
// read upstream header und re-use it
|
||||||
|
if m.o.upstreamReqIdHeader != "" {
|
||||||
|
reqId = r.Header.Get(m.o.upstreamReqIdHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate new id
|
||||||
|
if reqId == "" && m.o.generateLength > 0 {
|
||||||
|
reqId = m.generateRandomId()
|
||||||
|
}
|
||||||
|
|
||||||
|
// set response header
|
||||||
|
if m.o.headerIdentifier != "" {
|
||||||
|
w.Header().Set(m.o.headerIdentifier, reqId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set context value
|
||||||
|
if m.o.contextIdentifier != "" {
|
||||||
|
ctx := context.WithValue(r.Context(), m.o.contextIdentifier, reqId)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r) // execute the next handler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// region internal-helpers
|
||||||
|
|
||||||
|
func (m *Middleware) generateRandomId() string {
|
||||||
|
b := make([]byte, m.o.generateLength)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = m.o.generateCharset[m.seededRand.Intn(len(m.o.generateCharset))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion internal-helpers
|
118
internal/app/api/core/middleware/tracing/middleware_test.go
Normal file
118
internal/app/api/core/middleware/tracing/middleware_test.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package tracing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultLength = 8
|
||||||
|
const upstreamHeaderValue = "upstream-id"
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_WithUpstreamHeader(t *testing.T) {
|
||||||
|
m := New(WithUpstreamHeader("X-Upstream-Id"))
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqId := r.Header.Get("X-Upstream-Id")
|
||||||
|
if reqId != upstreamHeaderValue {
|
||||||
|
t.Errorf("expected upstream request id to be 'upstream-id', got %s", reqId)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Set("X-Upstream-Id", upstreamHeaderValue)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if rr.Header().Get("X-Request-Id") != upstreamHeaderValue {
|
||||||
|
t.Errorf("expected X-Request-Id header to be set in the response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_GenerateNewId(t *testing.T) {
|
||||||
|
idLen := 18
|
||||||
|
m := New(WithIdLength(idLen))
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqId := w.Header().Get("X-Request-Id")
|
||||||
|
if len(reqId) != 18 {
|
||||||
|
t.Errorf("expected generated request id length to be %d, got %d", idLen, len(reqId))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if rr.Header().Get("X-Request-Id") == "" || len(rr.Header().Get("X-Request-Id")) != idLen {
|
||||||
|
t.Errorf("expected X-Request-Id header to be set in the response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_SetContextValue(t *testing.T) {
|
||||||
|
m := New()
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqId := r.Context().Value("RequestId").(string)
|
||||||
|
if reqId == "" || len(reqId) != defaultLength {
|
||||||
|
t.Errorf("expected context request id to be set, got empty string")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_SetCustomContextValue(t *testing.T) {
|
||||||
|
m := New(WithContextIdentifier("Custom-Id"))
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqId := r.Context().Value("Custom-Id").(string)
|
||||||
|
if reqId == "" || len(reqId) != defaultLength {
|
||||||
|
t.Errorf("expected context request id to be set, got empty string")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_NoIdGenerated(t *testing.T) {
|
||||||
|
m := New(WithIdLength(0))
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqId := w.Header().Get("X-Request-Id")
|
||||||
|
if reqId != "" {
|
||||||
|
t.Errorf("expected no request id to be generated, got %s", reqId)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_NoIdHeaderSet(t *testing.T) {
|
||||||
|
m := New(WithHeaderIdentifier(""))
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqId := w.Header().Get("X-Request-Id")
|
||||||
|
if reqId != "" {
|
||||||
|
t.Errorf("expected no request id to be generated, got %s", reqId)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_NoIdContextSet(t *testing.T) {
|
||||||
|
m := New(WithHeaderIdentifier(""))
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
reqId := r.Context().Value("Request-Id")
|
||||||
|
if reqId != nil {
|
||||||
|
t.Errorf("expected no context request id to be set, got %v", reqId)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
}
|
85
internal/app/api/core/middleware/tracing/options.go
Normal file
85
internal/app/api/core/middleware/tracing/options.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package tracing
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// options is a struct that contains options for the tracing middleware.
|
||||||
|
// It uses the functional options pattern for flexible configuration.
|
||||||
|
type options struct {
|
||||||
|
upstreamReqIdHeader string
|
||||||
|
headerIdentifier string
|
||||||
|
contextIdentifier string
|
||||||
|
generateLength int
|
||||||
|
generateCharset string
|
||||||
|
generateSeed int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a type that is used to set options for the tracing middleware.
|
||||||
|
// It implements the functional options pattern.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
// WithIdSeed sets the seed for the random request id.
|
||||||
|
// If no seed is provided, the current timestamp is used.
|
||||||
|
func WithIdSeed(seed int64) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.generateSeed = seed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIdCharset sets the charset that is used to generate a random request id.
|
||||||
|
// By default, upper-case letters and numbers are used.
|
||||||
|
func WithIdCharset(charset string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.generateCharset = charset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIdLength specifies the length of generated random ids.
|
||||||
|
// By default, a length of 8 is used. If the length is 0, no request id will be generated.
|
||||||
|
func WithIdLength(len int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.generateLength = len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeaderIdentifier specifies the header name for the request id that is added to the response headers.
|
||||||
|
// If the identifier is empty, the request id will not be added to the response headers.
|
||||||
|
func WithHeaderIdentifier(identifier string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.headerIdentifier = identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUpstreamHeader sets the upstream header name, that should be used to fetch the request id.
|
||||||
|
// If no upstream header is found, a random id will be generated if the id-length parameter is set to a value > 0.
|
||||||
|
func WithUpstreamHeader(header string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.upstreamReqIdHeader = header
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContextIdentifier specifies the value-key for the request id that is added to the request context.
|
||||||
|
// If the identifier is empty, the request id will not be added to the context.
|
||||||
|
// If the request id is added to the context, it can be retrieved with:
|
||||||
|
// `id := r.Context().Value(THE-IDENTIFIER).(string)`
|
||||||
|
func WithContextIdentifier(identifier string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.contextIdentifier = identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newOptions is a function that returns a new options struct with sane default values.
|
||||||
|
func newOptions(opts ...Option) options {
|
||||||
|
o := options{
|
||||||
|
headerIdentifier: "X-Request-Id",
|
||||||
|
contextIdentifier: "RequestId",
|
||||||
|
generateSeed: time.Now().UnixNano(),
|
||||||
|
generateCharset: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
||||||
|
generateLength: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user