mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-05 07:56:17 +00:00
Compare commits
10 Commits
v2.0.0-alp
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
31c0daeba8 | ||
|
662e9c0549 | ||
|
6523a87dfb | ||
|
7ccec5db8d | ||
|
c211c56f75 | ||
|
17844ed929 | ||
|
2d78fe33b8 | ||
|
63d85d8123 | ||
|
26d3257516 | ||
|
d596f578f6 |
2
.github/workflows/pages.yml
vendored
2
.github/workflows/pages.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin
|
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
||||||
|
|
||||||
- name: Publish documentation
|
- name: Publish documentation
|
||||||
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,8 +32,6 @@ ssh.key
|
|||||||
.testCoverage.txt
|
.testCoverage.txt
|
||||||
wg_portal.db
|
wg_portal.db
|
||||||
sqlite.db
|
sqlite.db
|
||||||
swagger.json
|
|
||||||
swagger.yaml
|
|
||||||
/config.yml
|
/config.yml
|
||||||
/config/
|
/config/
|
||||||
venv/
|
venv/
|
||||||
|
6
Makefile
6
Makefile
@@ -133,3 +133,9 @@ build-docker:
|
|||||||
.PHONY: helm-docs
|
.PHONY: helm-docs
|
||||||
helm-docs:
|
helm-docs:
|
||||||
docker run --rm --volume "${PWD}/deploy:/helm-docs" -u "$$(id -u)" jnorwood/helm-docs -s file
|
docker run --rm --volume "${PWD}/deploy:/helm-docs" -u "$$(id -u)" jnorwood/helm-docs -s file
|
||||||
|
|
||||||
|
#< run-mkdocs: Run a local instance of MkDocs
|
||||||
|
.PHONY: run-mkdocs
|
||||||
|
run-mkdocs:
|
||||||
|
python -m venv venv; source venv/bin/activate; pip install mike cairosvg mkdocs-material mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
||||||
|
venv/bin/mkdocs serve
|
||||||
|
199
README.md
199
README.md
@@ -38,7 +38,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL or Post
|
|||||||
* Peer Expiry Feature
|
* Peer Expiry Feature
|
||||||
* Handle route and DNS settings like wg-quick does
|
* Handle route and DNS settings like wg-quick does
|
||||||
* Exposes Prometheus [metrics](#metrics)
|
* Exposes Prometheus [metrics](#metrics)
|
||||||
* ~~REST API for management and client deployment~~ (coming soon)
|
* REST API for management and client deployment
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -55,97 +55,107 @@ By default, WireGuard Portal uses a SQLite database. The database is stored in *
|
|||||||
### Configuration Options
|
### Configuration Options
|
||||||
The following configuration options are available:
|
The following configuration options are available:
|
||||||
|
|
||||||
| configuration key | parent key | default_value | description |
|
| 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_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. |
|
| 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. |
|
| 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 | 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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_level | advanced | info | The loglevel, can be one of: trace, debug, info, warn, error. |
|
||||||
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
|
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
|
||||||
| log_json | advanced | false | Logs in JSON format. |
|
| 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_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_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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
|
||||||
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
|
| 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 |
|
||||||
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
|
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
|
||||||
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
|
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
|
||||||
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
|
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
|
||||||
| data_collection_interval | statistics | 1m | The interval between the data collection cycles. |
|
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
|
||||||
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
|
| data_collection_interval | statistics | 1m | The interval between the data collection cycles. |
|
||||||
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
|
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
|
||||||
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
|
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
|
||||||
| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. |
|
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
|
||||||
| host | mail | 127.0.0.1 | The mail-server address. |
|
| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. |
|
||||||
| port | mail | 25 | The mail-server SMTP port. |
|
| host | mail | 127.0.0.1 | The mail-server address. |
|
||||||
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
|
| port | mail | 25 | The mail-server SMTP port. |
|
||||||
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
|
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
|
||||||
| username | mail | | The SMTP user name. |
|
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
|
||||||
| password | mail | | The SMTP password. |
|
| username | mail | | The SMTP user name. |
|
||||||
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
|
| password | mail | | The SMTP password. |
|
||||||
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
|
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
|
||||||
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
|
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
|
||||||
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
|
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
|
||||||
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
|
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc 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. |
|
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth 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). |
|
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
|
||||||
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
|
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
||||||
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
|
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
|
||||||
| client_id | auth/oidc | | The OAuth client id. |
|
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
|
||||||
| client_secret | auth/oidc | | The OAuth client secret. |
|
| client_id | auth/oidc | | The OAuth client id. |
|
||||||
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
|
| client_secret | auth/oidc | | The OAuth client secret. |
|
||||||
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
|
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
|
||||||
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department, is_admin and user_groups. |
|
||||||
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
| admin_mapping | auth/oidc | | Contains regex values admin_value_regex and admin_group_regex to map the is_admin field and user_groups respectively. |
|
||||||
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
|
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||||
| client_id | auth/oauth | | The OAuth client id. |
|
| log_user_info | auth/oidc | | If true, the user info retrieved from the OIDC provider will be logged in trace level. |
|
||||||
| client_secret | auth/oauth | | The OAuth client secret. |
|
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
||||||
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
|
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
|
||||||
| token_url | auth/oauth | | The URL for the token endpoint. |
|
| client_id | auth/oauth | | The OAuth client id. |
|
||||||
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
|
| client_secret | auth/oauth | | The OAuth client secret. |
|
||||||
| scopes | auth/oauth | | OAuth scopes. |
|
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
|
||||||
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
|
| token_url | auth/oauth | | The URL for the token endpoint. |
|
||||||
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
|
||||||
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
|
| scopes | auth/oauth | | OAuth scopes. |
|
||||||
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
|
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin and user_groups. |
|
||||||
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
|
| admin_mapping | auth/oauth | | Contains regex values admin_value_regex and admin_group_regex to map the is_admin field and user_groups respectively. |
|
||||||
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
|
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||||
| tls_key_path | auth/ldap | | A path to the TLS key. |
|
| log_user_info | auth/oauth | | If true, the user info retrieved from the OAuth provider will be logged in trace level. |
|
||||||
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
|
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
|
||||||
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
|
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
|
||||||
| bind_pass | auth/ldap | | The bind password. |
|
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
|
||||||
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
|
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
|
||||||
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
|
| tls_key_path | auth/ldap | | A path to the TLS key. |
|
||||||
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
|
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
|
||||||
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
|
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
|
||||||
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
|
| bind_pass | auth/ldap | | The bind password. |
|
||||||
| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. |
|
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
|
||||||
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
|
||||||
| debug | database | false | Debug database statements (log each statement). |
|
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
|
||||||
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
|
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
|
||||||
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
|
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
|
||||||
| 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 |
|
| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. |
|
||||||
| request_logging | web | false | Log all HTTP requests. |
|
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||||
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
|
| log_user_info | auth/ldap | | If true, the user info retrieved from the LDAP provider will be logged in trace level. |
|
||||||
| listening_address | web | :8888 | The listening port of the web server. |
|
| debug | database | false | Debug database statements (log each statement). |
|
||||||
| session_identifier | web | wgPortalSession | The session identifier for the web frontend. |
|
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
|
||||||
| session_secret | web | very_secret | The session secret for the web frontend. |
|
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
|
||||||
| csrf_secret | web | extremely_secret | The CSRF secret. |
|
| 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 |
|
||||||
| site_title | web | WireGuard Portal | The title that is shown in the web frontend. |
|
| request_logging | web | false | Log all HTTP requests. |
|
||||||
| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. |
|
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
|
||||||
| cert_file | web | | (Optional) Path to the TLS certificate file |
|
| listening_address | web | :8888 | The listening port of the web server. |
|
||||||
| key_file | web | | (Optional) Path to the TLS certificate key file |
|
| 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
|
## Upgrading from V1
|
||||||
|
|
||||||
@@ -172,16 +182,13 @@ Ensure that the new database does not contain any data!
|
|||||||
|
|
||||||
|
|
||||||
## V2 TODOs
|
## V2 TODOs
|
||||||
* Public REST API
|
|
||||||
* Translations
|
|
||||||
* Documentation
|
|
||||||
* Audit UI
|
* Audit UI
|
||||||
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
To build a standalone application, use the Makefile provided in the repository.
|
To build a standalone application, use the Makefile provided in the repository.
|
||||||
Go version 1.22 or higher has to be installed to build WireGuard Portal.
|
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.
|
If you want to re-compile the frontend, NodeJS 18 and NPM >= 9 is required.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@@ -12,6 +12,10 @@ import (
|
|||||||
"github.com/swaggo/swag/gen"
|
"github.com/swaggo/swag/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var apiRootPath = "/internal/app/api"
|
||||||
|
var apiDocPath = "core/assets/doc"
|
||||||
|
var apiMkDocPath = "/docs/documentation/rest-api"
|
||||||
|
|
||||||
// this replaces the call to: swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo base.go
|
// this replaces the call to: swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo base.go
|
||||||
func main() {
|
func main() {
|
||||||
wd, err := os.Getwd() // should be the project root
|
wd, err := os.Getwd() // should be the project root
|
||||||
@@ -19,8 +23,8 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiBasePath := filepath.Join(wd, "/internal/app/api")
|
apiBasePath := filepath.Join(wd, apiRootPath)
|
||||||
apis := []string{"v0"}
|
apis := []string{"v0", "v1"}
|
||||||
|
|
||||||
hasError := false
|
hasError := false
|
||||||
for _, apiVersion := range apis {
|
for _, apiVersion := range apis {
|
||||||
@@ -37,6 +41,15 @@ func main() {
|
|||||||
logrus.Errorf("failed to generate API docs for %s: %v", apiVersion, err)
|
logrus.Errorf("failed to generate API docs for %s: %v", apiVersion, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copy the latest version of the API docs for mkdocs
|
||||||
|
if apiVersion == apis[len(apis)-1] {
|
||||||
|
if err = copyDocForMkdocs(wd, apiBasePath, apiVersion); err != nil {
|
||||||
|
logrus.Errorf("failed to copy API docs for mkdocs: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("Copied API docs " + apiVersion + " for mkdocs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Println("Generated swagger docs for API", apiVersion)
|
log.Println("Generated swagger docs for API", apiVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +64,7 @@ func generateApi(basePath, apiPath, version string) error {
|
|||||||
Excludes: "",
|
Excludes: "",
|
||||||
MainAPIFile: "base.go",
|
MainAPIFile: "base.go",
|
||||||
PropNamingStrategy: swag.PascalCase,
|
PropNamingStrategy: swag.PascalCase,
|
||||||
OutputDir: filepath.Join(basePath, "core/assets/doc"),
|
OutputDir: filepath.Join(basePath, apiDocPath),
|
||||||
OutputTypes: []string{"json", "yaml"},
|
OutputTypes: []string{"json", "yaml"},
|
||||||
ParseVendor: false,
|
ParseVendor: false,
|
||||||
ParseDependency: 3,
|
ParseDependency: 3,
|
||||||
@@ -68,3 +81,21 @@ func generateApi(basePath, apiPath, version string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyDocForMkdocs(workingDir, basePath, version string) error {
|
||||||
|
srcPath := filepath.Join(basePath, apiDocPath, fmt.Sprintf("%s_swagger.yaml", version))
|
||||||
|
dstPath := filepath.Join(workingDir, apiMkDocPath, "swagger.yaml")
|
||||||
|
|
||||||
|
// copy the file
|
||||||
|
input, err := os.ReadFile(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while reading swagger doc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(dstPath, input, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while writing swagger doc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@@ -9,6 +9,8 @@ import (
|
|||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/app/api/core"
|
"github.com/h44z/wg-portal/internal/app/api/core"
|
||||||
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"
|
||||||
|
handlersV1 "github.com/h44z/wg-portal/internal/app/api/v1/handlers"
|
||||||
"github.com/h44z/wg-portal/internal/app/audit"
|
"github.com/h44z/wg-portal/internal/app/audit"
|
||||||
"github.com/h44z/wg-portal/internal/app/auth"
|
"github.com/h44z/wg-portal/internal/app/auth"
|
||||||
"github.com/h44z/wg-portal/internal/app/configfile"
|
"github.com/h44z/wg-portal/internal/app/configfile"
|
||||||
@@ -103,7 +105,27 @@ func main() {
|
|||||||
|
|
||||||
apiFrontend := handlersV0.NewRestApi(cfg, backend)
|
apiFrontend := handlersV0.NewRestApi(cfg, backend)
|
||||||
|
|
||||||
webSrv, err := core.NewServer(cfg, apiFrontend)
|
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
|
||||||
|
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
|
||||||
|
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
|
||||||
|
apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager)
|
||||||
|
apiV1BackendMetrics := backendV1.NewMetricsService(cfg, database, userManager, wireGuardManager)
|
||||||
|
apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1BackendUsers)
|
||||||
|
apiV1EndpointPeers := handlersV1.NewPeerEndpoint(apiV1BackendPeers)
|
||||||
|
apiV1EndpointInterfaces := handlersV1.NewInterfaceEndpoint(apiV1BackendInterfaces)
|
||||||
|
apiV1EndpointProvisioning := handlersV1.NewProvisioningEndpoint(apiV1BackendProvisioning)
|
||||||
|
apiV1EndpointMetrics := handlersV1.NewMetricsEndpoint(apiV1BackendMetrics)
|
||||||
|
|
||||||
|
apiV1 := handlersV1.NewRestApi(
|
||||||
|
userManager,
|
||||||
|
apiV1EndpointUsers,
|
||||||
|
apiV1EndpointPeers,
|
||||||
|
apiV1EndpointInterfaces,
|
||||||
|
apiV1EndpointProvisioning,
|
||||||
|
apiV1EndpointMetrics,
|
||||||
|
)
|
||||||
|
|
||||||
|
webSrv, err := core.NewServer(cfg, apiFrontend, apiV1)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
go metricsServer.Run(ctx)
|
go metricsServer.Run(ctx)
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
# More information about the configuration can be found in the documentation: https://wgportal.org/master/documentation/overview/
|
||||||
|
|
||||||
advanced:
|
advanced:
|
||||||
log_level: trace
|
log_level: trace
|
||||||
|
|
||||||
@@ -22,7 +24,7 @@ auth:
|
|||||||
base_dn: DC=YOURCOMPANY,DC=LOCAL
|
base_dn: DC=YOURCOMPANY,DC=LOCAL
|
||||||
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||||
admin_group: CN=WireGuardAdmins,OU=it,DC=YOURCOMPANY,DC=LOCAL
|
admin_group: CN=WireGuardAdmins,OU=it,DC=YOURCOMPANY,DC=LOCAL
|
||||||
synchronize: false
|
sync_interval: 0 # sync disabled
|
||||||
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||||
registration_enabled: true
|
registration_enabled: true
|
||||||
oidc:
|
oidc:
|
||||||
@@ -63,5 +65,28 @@ auth:
|
|||||||
email: email
|
email: email
|
||||||
firstname: name
|
firstname: name
|
||||||
user_identifier: sub
|
user_identifier: sub
|
||||||
is_admin: roles
|
is_admin: this-attribute-must-be-true
|
||||||
registration_enabled: true
|
registration_enabled: true
|
||||||
|
- id: google_plain_oauth_with_groups
|
||||||
|
provider_name: google4
|
||||||
|
display_name: Login with</br>Google4
|
||||||
|
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||||
|
client_secret: A_CLIENT_SECRET
|
||||||
|
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||||
|
token_url: https://oauth2.googleapis.com/token
|
||||||
|
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||||
|
scopes:
|
||||||
|
- openid
|
||||||
|
- email
|
||||||
|
- profile
|
||||||
|
- i-want-some-groups
|
||||||
|
field_map:
|
||||||
|
email: email
|
||||||
|
firstname: name
|
||||||
|
user_identifier: sub
|
||||||
|
user_groups: groups
|
||||||
|
admin_mapping:
|
||||||
|
admin_value_regex: ^true$
|
||||||
|
admin_group_regex: ^admin-group-name$
|
||||||
|
registration_enabled: true
|
||||||
|
log_user_info: true
|
176
docs/documentation/configuration/examples.md
Normal file
176
docs/documentation/configuration/examples.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
Below are some sample YAML configurations demonstrating how to override some default values.
|
||||||
|
|
||||||
|
## Basic Configuration
|
||||||
|
```yaml
|
||||||
|
core:
|
||||||
|
admin_user: test@example.com
|
||||||
|
admin_password: password
|
||||||
|
import_existing: false
|
||||||
|
create_default_peer: true
|
||||||
|
self_provisioning_allowed: true
|
||||||
|
|
||||||
|
web:
|
||||||
|
site_title: My WireGuard Server
|
||||||
|
site_company_name: My Company
|
||||||
|
listening_address: :8080
|
||||||
|
external_url: https://my.externa-domain.com
|
||||||
|
csrf_secret: super-s3cr3t-csrf
|
||||||
|
session_secret: super-s3cr3t-session
|
||||||
|
request_logging: true
|
||||||
|
|
||||||
|
advanced:
|
||||||
|
log_level: trace
|
||||||
|
log_pretty: true
|
||||||
|
log_json: false
|
||||||
|
config_storage_path: /etc/wireguard
|
||||||
|
expiry_check_interval: 5m
|
||||||
|
|
||||||
|
database:
|
||||||
|
debug: true
|
||||||
|
type: sqlite
|
||||||
|
dsn: data/sqlite.db
|
||||||
|
```
|
||||||
|
|
||||||
|
## LDAP Authentication and Synchronization Configuration
|
||||||
|
```yaml
|
||||||
|
# ... (basic configuration)
|
||||||
|
|
||||||
|
auth:
|
||||||
|
ldap:
|
||||||
|
|
||||||
|
# a sample LDAP provider with user sync enabled
|
||||||
|
- id: ldap
|
||||||
|
provider_name: Active Directory
|
||||||
|
display_name: Login with</br>AD
|
||||||
|
url: ldap://srv-ad1.company.local:389
|
||||||
|
bind_user: ldap_wireguard@company.local
|
||||||
|
bind_pass: super-s3cr3t-ldap
|
||||||
|
base_dn: DC=COMPANY,DC=LOCAL
|
||||||
|
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||||
|
sync_interval: 15m
|
||||||
|
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||||
|
disable_missing: true
|
||||||
|
field_map:
|
||||||
|
user_identifier: sAMAccountName
|
||||||
|
email: mail
|
||||||
|
firstname: givenName
|
||||||
|
lastname: sn
|
||||||
|
phone: telephoneNumber
|
||||||
|
department: department
|
||||||
|
memberof: memberOf
|
||||||
|
admin_group: CN=WireGuardAdmins,OU=Some-OU,DC=COMPANY,DC=LOCAL
|
||||||
|
registration_enabled: true
|
||||||
|
log_user_info: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenID Connect (OIDC) Authentication Configuration
|
||||||
|
```yaml
|
||||||
|
# ... (basic configuration)
|
||||||
|
|
||||||
|
auth:
|
||||||
|
oidc:
|
||||||
|
|
||||||
|
# a sample provider where users with the attribute `wg_admin` set to `true` are considered as admins
|
||||||
|
- id: oidc-with-admin-attribute
|
||||||
|
provider_name: google
|
||||||
|
display_name: Login with</br>Google
|
||||||
|
base_url: https://accounts.google.com
|
||||||
|
client_id: the-client-id-1234.apps.googleusercontent.com
|
||||||
|
client_secret: A_CLIENT_SECRET
|
||||||
|
extra_scopes:
|
||||||
|
- https://www.googleapis.com/auth/userinfo.email
|
||||||
|
- https://www.googleapis.com/auth/userinfo.profile
|
||||||
|
field_map:
|
||||||
|
user_identifier: sub
|
||||||
|
email: email
|
||||||
|
firstname: given_name
|
||||||
|
lastname: family_name
|
||||||
|
phone: phone_number
|
||||||
|
department: department
|
||||||
|
is_admin: wg_admin
|
||||||
|
admin_mapping:
|
||||||
|
- admin_value_regex: ^true$
|
||||||
|
registration_enabled: true
|
||||||
|
log_user_info: true
|
||||||
|
|
||||||
|
# a sample provider where users in the group `the-admin-group` are considered as admins
|
||||||
|
- id: oidc-with-admin-group
|
||||||
|
provider_name: google2
|
||||||
|
display_name: Login with</br>Google2
|
||||||
|
base_url: https://accounts.google.com
|
||||||
|
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||||
|
client_secret: A_CLIENT_SECRET
|
||||||
|
extra_scopes:
|
||||||
|
- https://www.googleapis.com/auth/userinfo.email
|
||||||
|
- https://www.googleapis.com/auth/userinfo.profile
|
||||||
|
field_map:
|
||||||
|
user_identifier: sub
|
||||||
|
email: email
|
||||||
|
firstname: given_name
|
||||||
|
lastname: family_name
|
||||||
|
phone: phone_number
|
||||||
|
department: department
|
||||||
|
user_groups: groups
|
||||||
|
admin_mapping:
|
||||||
|
- admin_group_regex: ^the-admin-group$
|
||||||
|
registration_enabled: true
|
||||||
|
log_user_info: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plain OAuth2 Authentication Configuration
|
||||||
|
```yaml
|
||||||
|
# ... (basic configuration)
|
||||||
|
|
||||||
|
auth:
|
||||||
|
oauth:
|
||||||
|
|
||||||
|
# a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True`
|
||||||
|
# are considered as admins
|
||||||
|
- id: google_plain_oauth-with-admin-attribute
|
||||||
|
provider_name: google3
|
||||||
|
display_name: Login with</br>Google3
|
||||||
|
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||||
|
client_secret: A_CLIENT_SECRET
|
||||||
|
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||||
|
token_url: https://oauth2.googleapis.com/token
|
||||||
|
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||||
|
scopes:
|
||||||
|
- openid
|
||||||
|
- email
|
||||||
|
- profile
|
||||||
|
field_map:
|
||||||
|
user_identifier: sub
|
||||||
|
email: email
|
||||||
|
firstname: name
|
||||||
|
is_admin: this-attribute-must-be-true
|
||||||
|
admin_mapping:
|
||||||
|
- admin_value_regex: ^(True|true)$
|
||||||
|
registration_enabled: true
|
||||||
|
|
||||||
|
# a sample provider where either users with the attribute `this-attribute-must-be-true` set to `true` or
|
||||||
|
# users in the group `admin-group-name` are considered as admins
|
||||||
|
- id: google_plain_oauth_with_groups
|
||||||
|
provider_name: google4
|
||||||
|
display_name: Login with</br>Google4
|
||||||
|
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||||
|
client_secret: A_CLIENT_SECRET
|
||||||
|
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||||
|
token_url: https://oauth2.googleapis.com/token
|
||||||
|
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||||
|
scopes:
|
||||||
|
- openid
|
||||||
|
- email
|
||||||
|
- profile
|
||||||
|
- i-want-some-groups
|
||||||
|
field_map:
|
||||||
|
email: email
|
||||||
|
firstname: name
|
||||||
|
user_identifier: sub
|
||||||
|
is_admin: this-attribute-must-be-true
|
||||||
|
user_groups: groups
|
||||||
|
admin_mapping:
|
||||||
|
admin_value_regex: ^true$
|
||||||
|
admin_group_regex: ^admin-group-name$
|
||||||
|
registration_enabled: true
|
||||||
|
log_user_info: true
|
||||||
|
```
|
453
docs/documentation/configuration/overview.md
Normal file
453
docs/documentation/configuration/overview.md
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
# WireGuard Portal Configuration
|
||||||
|
|
||||||
|
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`.
|
||||||
|
Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core
|
||||||
|
|
||||||
|
These are the primary configuration options that control fundamental WireGuard Portal behavior.
|
||||||
|
More advanced options are found in the subsequent `Advanced` section.
|
||||||
|
|
||||||
|
### `admin_user`
|
||||||
|
- **Default:** `admin@wgportal.local`
|
||||||
|
- **Description:** The administrator user. This user will be created as a default admin if it does not yet exist.
|
||||||
|
|
||||||
|
### `admin_password`
|
||||||
|
- **Default:** `wgportal`
|
||||||
|
- **Description:** The administrator password. The default password of `wgportal` should be changed immediately.
|
||||||
|
|
||||||
|
### `editable_keys`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** Allow editing of WireGuard key-pairs directly in the UI.
|
||||||
|
|
||||||
|
### `create_default_peer`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||||
|
|
||||||
|
### `create_default_peer_on_creation`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||||
|
|
||||||
|
### `re_enable_peer_after_user_enable`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** Re-enable all peers that were previously disabled if the associated user is re-enabled.
|
||||||
|
|
||||||
|
### `delete_peer_after_user_deleted`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If a user is deleted, remove all linked peers. Otherwise, peers remain but are disabled.
|
||||||
|
|
||||||
|
### `self_provisioning_allowed`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** Allow registered (non-admin) users to self-provision peers from their profile page.
|
||||||
|
|
||||||
|
### `import_existing`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** On startup, import existing WireGuard interfaces and peers into WireGuard Portal.
|
||||||
|
|
||||||
|
### `restore_state`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** Restore the WireGuard interface states (up/down) that existed before WireGuard Portal started.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced
|
||||||
|
|
||||||
|
Additional or more specialized configuration options for logging and interface creation details.
|
||||||
|
|
||||||
|
### `log_level`
|
||||||
|
- **Default:** `info`
|
||||||
|
- **Description:** The log level used by the application. Valid options are: `trace`, `debug`, `info`, `warn`, `error`.
|
||||||
|
|
||||||
|
### `log_pretty`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If `true`, log messages are colorized and formatted for readability (pretty-print).
|
||||||
|
|
||||||
|
### `log_json`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If `true`, log messages are structured in JSON format.
|
||||||
|
|
||||||
|
### `start_listen_port`
|
||||||
|
- **Default:** `51820`
|
||||||
|
- **Description:** The first port to use when automatically creating new WireGuard interfaces.
|
||||||
|
|
||||||
|
### `start_cidr_v4`
|
||||||
|
- **Default:** `10.11.12.0/24`
|
||||||
|
- **Description:** The initial IPv4 subnet to use when automatically creating new WireGuard interfaces.
|
||||||
|
|
||||||
|
### `start_cidr_v6`
|
||||||
|
- **Default:** `fdfd:d3ad:c0de:1234::0/64`
|
||||||
|
- **Description:** The initial IPv6 subnet to use when automatically creating new WireGuard interfaces.
|
||||||
|
|
||||||
|
### `use_ip_v6`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** Enable or disable IPv6 support.
|
||||||
|
|
||||||
|
### `config_storage_path`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Path to a directory where `wg-quick` style configuration files will be stored (if you need local filesystem configs).
|
||||||
|
|
||||||
|
### `expiry_check_interval`
|
||||||
|
- **Default:** `15m`
|
||||||
|
- **Description:** Interval after which existing peers are checked if they are expired. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||||
|
|
||||||
|
### `rule_prio_offset`
|
||||||
|
- **Default:** `20000`
|
||||||
|
- **Description:** Offset for IP route rule priorities when configuring routing.
|
||||||
|
|
||||||
|
### `route_table_offset`
|
||||||
|
- **Default:** `20000`
|
||||||
|
- **Description:** Offset for IP route table IDs when configuring routing.
|
||||||
|
|
||||||
|
### `api_admin_only`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
Configuration for the underlying database used by WireGuard Portal.
|
||||||
|
Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
|
||||||
|
|
||||||
|
### `debug`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If `true`, logs all database statements (verbose).
|
||||||
|
|
||||||
|
### `slow_query_threshold`
|
||||||
|
- **Default:** 0
|
||||||
|
- **Description:** A time threshold (e.g., `100ms`) above which queries are considered slow and logged as warnings. If empty or zero, slow query logging is disabled. Format uses `s`, `ms` for seconds, milliseconds, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||||
|
|
||||||
|
### `type`
|
||||||
|
- **Default:** `sqlite`
|
||||||
|
- **Description:** The database type. Valid options: `sqlite`, `mssql`, `mysql`, `postgres`.
|
||||||
|
|
||||||
|
### `dsn`
|
||||||
|
- **Default:** `data/sqlite.db`
|
||||||
|
- **Description:** The Data Source Name (DSN) for connecting to the database.
|
||||||
|
For example:
|
||||||
|
```text
|
||||||
|
user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Statistics
|
||||||
|
|
||||||
|
Controls how WireGuard Portal collects and reports usage statistics, including ping checks and Prometheus metrics.
|
||||||
|
|
||||||
|
### `use_ping_checks`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** Enable periodic ping checks to verify that peers remain responsive.
|
||||||
|
|
||||||
|
### `ping_check_workers`
|
||||||
|
- **Default:** `10`
|
||||||
|
- **Description:** Number of parallel worker processes for ping checks.
|
||||||
|
|
||||||
|
### `ping_unprivileged`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If `false`, ping checks run without root privileges. This is currently considered BETA.
|
||||||
|
|
||||||
|
### `ping_check_interval`
|
||||||
|
- **Default:** `1m`
|
||||||
|
- **Description:** Interval between consecutive ping checks for all peers. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||||
|
|
||||||
|
### `data_collection_interval`
|
||||||
|
- **Default:** `1m`
|
||||||
|
- **Description:** Interval between data collection cycles (bytes sent/received, handshake times, etc.). Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||||
|
|
||||||
|
### `collect_interface_data`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** If `true`, collects interface-level data (bytes in/out) for monitoring and statistics.
|
||||||
|
|
||||||
|
### `collect_peer_data`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** If `true`, collects peer-level data (bytes, last handshake, endpoint, etc.).
|
||||||
|
|
||||||
|
### `collect_audit_data`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** If `true`, logs certain portal events (such as user logins) to the database.
|
||||||
|
|
||||||
|
### `listening_address`
|
||||||
|
- **Default:** `:8787`
|
||||||
|
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mail
|
||||||
|
|
||||||
|
Options for configuring email notifications or sending peer configurations via email.
|
||||||
|
|
||||||
|
### `host`
|
||||||
|
- **Default:** `127.0.0.1`
|
||||||
|
- **Description:** Hostname or IP of the SMTP server.
|
||||||
|
|
||||||
|
### `port`
|
||||||
|
- **Default:** `25`
|
||||||
|
- **Description:** Port number for the SMTP server.
|
||||||
|
|
||||||
|
### `encryption`
|
||||||
|
- **Default:** `none`
|
||||||
|
- **Description:** SMTP encryption type. Valid values: `none`, `tls`, `starttls`.
|
||||||
|
|
||||||
|
### `cert_validation`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If `true`, validate the SMTP server certificate (relevant if `encryption` = `tls`).
|
||||||
|
|
||||||
|
### `username`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Optional SMTP username for authentication.
|
||||||
|
|
||||||
|
### `password`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Optional SMTP password for authentication.
|
||||||
|
|
||||||
|
### `auth_type`
|
||||||
|
- **Default:** `plain`
|
||||||
|
- **Description:** SMTP authentication type. Valid values: `plain`, `login`, `crammd5`.
|
||||||
|
|
||||||
|
### `from`
|
||||||
|
- **Default:** `Wireguard Portal <noreply@wireguard.local>`
|
||||||
|
- **Description:** The default "From" address when sending emails.
|
||||||
|
|
||||||
|
### `link_only`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If `true`, emails only contain a link to WireGuard Portal, rather than attaching the full configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), and **LDAP** (`ldap`).
|
||||||
|
Each can have multiple providers configured. Below are the relevant keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### OIDC Provider Properties
|
||||||
|
|
||||||
|
The `oidc` array contains a list of OpenID Connect providers.
|
||||||
|
Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
||||||
|
|
||||||
|
#### `provider_name`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||||
|
|
||||||
|
#### `display_name`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A user-friendly name shown on the login page (e.g., "Login with Google").
|
||||||
|
|
||||||
|
#### `base_url`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The OIDC provider’s base URL (e.g., `https://accounts.google.com`).
|
||||||
|
|
||||||
|
#### `client_id`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The OAuth client ID from the OIDC provider.
|
||||||
|
|
||||||
|
#### `client_secret`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The OAuth client secret from the OIDC provider.
|
||||||
|
|
||||||
|
#### `extra_scopes`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A list of additional OIDC scopes (e.g., `profile`, `email`).
|
||||||
|
|
||||||
|
#### `field_map`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
|
||||||
|
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
||||||
|
|
||||||
|
| **Field** | **Typical OIDC Claim** | **Explanation** |
|
||||||
|
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
||||||
|
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
||||||
|
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
||||||
|
| `lastname` | `family_name` | The user’s last (family) name, typically provided by the IdP in the `family_name` claim. |
|
||||||
|
| `phone` | `phone_number` | The user’s phone number. This may require additional scopes/permissions from the IdP to access. |
|
||||||
|
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
|
||||||
|
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
|
||||||
|
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
|
||||||
|
|
||||||
|
#### `admin_mapping`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
|
||||||
|
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
|
||||||
|
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
|
||||||
|
|
||||||
|
#### `registration_enabled`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** If `true`, a new user will be created in WireGuard Portal if not already present.
|
||||||
|
|
||||||
|
#### `log_user_info`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** If `true`, OIDC user data is logged at the trace level upon login (for debugging).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### OAuth Provider Properties
|
||||||
|
|
||||||
|
The `oauth` array contains a list of plain OAuth2 providers.
|
||||||
|
Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
||||||
|
|
||||||
|
#### `provider_name`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||||
|
|
||||||
|
#### `display_name`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A user-friendly name shown on the login page.
|
||||||
|
|
||||||
|
#### `client_id`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The OAuth client ID for the provider.
|
||||||
|
|
||||||
|
#### `client_secret`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The OAuth client secret for the provider.
|
||||||
|
|
||||||
|
#### `auth_url`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** URL of the authentication endpoint.
|
||||||
|
|
||||||
|
#### `token_url`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** URL of the token endpoint.
|
||||||
|
|
||||||
|
#### `user_info_url`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** URL of the user information endpoint.
|
||||||
|
|
||||||
|
#### `scopes`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A list of OAuth scopes.
|
||||||
|
|
||||||
|
#### `field_map`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
|
||||||
|
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
||||||
|
|
||||||
|
| **Field** | **Typical Claim** | **Explanation** |
|
||||||
|
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
||||||
|
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
||||||
|
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
||||||
|
| `lastname` | `family_name` | The user’s last (family) name, typically provided by the IdP in the `family_name` claim. |
|
||||||
|
| `phone` | `phone_number` | The user’s phone number. This may require additional scopes/permissions from the IdP to access. |
|
||||||
|
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
|
||||||
|
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
|
||||||
|
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
|
||||||
|
|
||||||
|
#### `admin_mapping`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
|
||||||
|
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
|
||||||
|
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
|
||||||
|
|
||||||
|
#### `registration_enabled`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** If `true`, new users are created automatically on successful login.
|
||||||
|
|
||||||
|
#### `log_user_info`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** If `true`, logs user info at the trace level upon login.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LDAP Provider Properties
|
||||||
|
|
||||||
|
The `ldap` array contains a list of LDAP authentication providers.
|
||||||
|
Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
||||||
|
|
||||||
|
#### `url`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The LDAP server URL (e.g., `ldap://srv-ad01.company.local:389`).
|
||||||
|
|
||||||
|
#### `start_tls`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** If `true`, use STARTTLS to secure the LDAP connection.
|
||||||
|
|
||||||
|
#### `cert_validation`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** If `true`, validate the LDAP server’s TLS certificate.
|
||||||
|
|
||||||
|
#### `tls_certificate_path`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Path to a TLS certificate if needed for LDAP connections.
|
||||||
|
|
||||||
|
#### `tls_key_path`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Path to the corresponding TLS certificate key.
|
||||||
|
|
||||||
|
#### `base_dn`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The base DN for user searches (e.g., `DC=COMPANY,DC=LOCAL`).
|
||||||
|
|
||||||
|
#### `bind_user`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The bind user for LDAP (e.g., `company\\ldap_wireguard` or `ldap_wireguard@company.local`).
|
||||||
|
|
||||||
|
#### `bind_pass`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The bind password for LDAP authentication.
|
||||||
|
|
||||||
|
#### `field_map`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Maps LDAP attributes to WireGuard Portal fields.
|
||||||
|
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`.
|
||||||
|
|
||||||
|
| **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** |
|
||||||
|
|----------------------------|----------------------------|--------------------------------------------------------------|
|
||||||
|
| user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. |
|
||||||
|
| email | mail / userPrincipalName | Stores the user's primary email address. |
|
||||||
|
| firstname | givenName | Contains the user's first (given) name. |
|
||||||
|
| lastname | sn | Contains the user's last (surname) name. |
|
||||||
|
| phone | telephoneNumber / mobile | Holds the user's phone or mobile number. |
|
||||||
|
| department | departmentNumber / ou | Specifies the department or organizational unit of the user. |
|
||||||
|
| memberof | memberOf | Lists the groups and roles to which the user belongs. |
|
||||||
|
|
||||||
|
#### `login_filter`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** An LDAP filter to restrict which users can log in. Use `{{login_identifier}}` to insert the username.
|
||||||
|
For example:
|
||||||
|
```text
|
||||||
|
(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `admin_group`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A specific LDAP group whose members are considered administrators in WireGuard Portal.
|
||||||
|
For example:
|
||||||
|
```text
|
||||||
|
CN=WireGuardAdmins,OU=Some-OU,DC=YOURDOMAIN,DC=LOCAL
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `sync_interval`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** How frequently (in duration, e.g. `30m`) to synchronize users from LDAP. Empty or `0` disables sync. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||||
|
Only users that match the `sync_filter` are synchronized, if `disable_missing` is `true`, users not found in LDAP are disabled.
|
||||||
|
|
||||||
|
#### `sync_filter`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** An LDAP filter to select which users get synchronized into WireGuard Portal.
|
||||||
|
For example:
|
||||||
|
```text
|
||||||
|
(&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `disable_missing`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** If `true`, any user **not** found in LDAP (during sync) is disabled in WireGuard Portal.
|
||||||
|
|
||||||
|
#### `registration_enabled`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** If `true`, new user accounts are created in WireGuard Portal upon first login.
|
||||||
|
|
||||||
|
#### `log_user_info`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** If `true`, logs LDAP user data at the trace level upon login.
|
@@ -1,5 +1,5 @@
|
|||||||
To build a standalone application, use the Makefile provided in the repository.
|
To build a standalone application, use the Makefile provided in the repository.
|
||||||
Go version **1.22** or higher has to be installed to build WireGuard Portal.
|
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.
|
If you want to re-compile the frontend, NodeJS **18** and NPM >= **9** is required.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@@ -8,7 +8,7 @@ A sample docker-compose.yml:
|
|||||||
version: '3.6'
|
version: '3.6'
|
||||||
services:
|
services:
|
||||||
wg-portal:
|
wg-portal:
|
||||||
image: wgportal/wg-portal:v2
|
image: wgportal/wg-portal:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
@@ -64,18 +64,4 @@ You should mount those directories as a volume:
|
|||||||
- /app/data
|
- /app/data
|
||||||
- /app/config
|
- /app/config
|
||||||
|
|
||||||
### Configuration Options
|
A detailed description of the configuration options can be found [here](../configuration/overview.md).
|
||||||
All available YAML configuration options are available [here](https://github.com/h44z/wg-portal#configuration).
|
|
||||||
|
|
||||||
A very basic example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
core:
|
|
||||||
admin_user: test@wg-portal.local
|
|
||||||
admin_password: secret
|
|
||||||
|
|
||||||
web:
|
|
||||||
external_url: http://localhost:8888
|
|
||||||
request_logging: true
|
|
||||||
```
|
|
||||||
|
|
||||||
|
@@ -22,4 +22,15 @@ For example:
|
|||||||
```
|
```
|
||||||
|
|
||||||
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.
|
||||||
Ensure that the new database does not contain any data!
|
Ensure that the new database does not contain any data!
|
||||||
|
|
||||||
|
If you are using Docker, you can adapt the docker-compose.yml file to start the upgrade process:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
wg-portal:
|
||||||
|
image: wgportal/wg-portal:latest
|
||||||
|
# ... other settings
|
||||||
|
restart: no
|
||||||
|
command: ["-migrateFrom=/app/data/wg_portal.db"]
|
||||||
|
```
|
@@ -21,7 +21,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL or Post
|
|||||||
* Support for multiple WireGuard interfaces
|
* Support for multiple WireGuard interfaces
|
||||||
* Peer Expiry Feature
|
* Peer Expiry Feature
|
||||||
* Handle route and DNS settings like wg-quick does
|
* Handle route and DNS settings like wg-quick does
|
||||||
* ~~REST API for management and client deployment~~ (coming soon)
|
* REST API for management and client deployment
|
||||||
|
|
||||||
## Quick-Start
|
## Quick-Start
|
||||||
|
|
||||||
|
1
docs/documentation/rest-api/api-doc.md
Normal file
1
docs/documentation/rest-api/api-doc.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<swagger-ui src="./swagger.yaml"/>
|
1546
docs/documentation/rest-api/swagger.yaml
Normal file
1546
docs/documentation/rest-api/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -90,6 +90,7 @@ const currentYear = ref(new Date().getFullYear())
|
|||||||
href="#" role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
href="#" role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
||||||
<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>
|
||||||
<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>
|
||||||
|
@@ -88,6 +88,10 @@ function close() {
|
|||||||
<td>{{ $t('modals.user-view.department') }}:</td>
|
<td>{{ $t('modals.user-view.department') }}:</td>
|
||||||
<td>{{selectedUser.Department}}</td>
|
<td>{{selectedUser.Department}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ $t('modals.user-view.api-enabled') }}:</td>
|
||||||
|
<td>{{selectedUser.ApiEnabled}}</td>
|
||||||
|
</tr>
|
||||||
<tr v-if="selectedUser.Disabled">
|
<tr v-if="selectedUser.Disabled">
|
||||||
<td>{{ $t('modals.user-view.disabled') }}:</td>
|
<td>{{ $t('modals.user-view.disabled') }}:</td>
|
||||||
<td>{{selectedUser.DisabledReason}}</td>
|
<td>{{selectedUser.DisabledReason}}</td>
|
||||||
|
@@ -146,6 +146,8 @@ export function freshUser() {
|
|||||||
Locked: false,
|
Locked: false,
|
||||||
LockedReason: "",
|
LockedReason: "",
|
||||||
|
|
||||||
|
ApiEnabled: false,
|
||||||
|
|
||||||
PeerCount: 0
|
PeerCount: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -37,6 +37,7 @@
|
|||||||
"users": "Benutzer",
|
"users": "Benutzer",
|
||||||
"lang": "Sprache ändern",
|
"lang": "Sprache ändern",
|
||||||
"profile": "Mein Profil",
|
"profile": "Mein Profil",
|
||||||
|
"settings": "Einstellungen",
|
||||||
"login": "Anmelden",
|
"login": "Anmelden",
|
||||||
"logout": "Abmelden"
|
"logout": "Abmelden"
|
||||||
},
|
},
|
||||||
@@ -167,6 +168,26 @@
|
|||||||
"button-show-peer": "Show Peer",
|
"button-show-peer": "Show Peer",
|
||||||
"button-edit-peer": "Edit Peer"
|
"button-edit-peer": "Edit Peer"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"headline": "Einstellungen",
|
||||||
|
"abstract": "Hier finden Sie persönliche Einstellungen für WireGuard Portal.",
|
||||||
|
"api": {
|
||||||
|
"headline": "API Einstellungen",
|
||||||
|
"abstract": "Hier können Sie die RESTful API verwalten.",
|
||||||
|
"active-description": "Die API ist derzeit für Ihr Benutzerkonto aktiv. Alle API-Anfragen werden mit Basic Auth authentifiziert. Verwenden Sie zur Authentifizierung die folgenden Anmeldeinformationen.",
|
||||||
|
"inactive-description": "Die API ist derzeit inaktiv. Klicken Sie auf die Schaltfläche unten, um sie zu aktivieren.",
|
||||||
|
"user-label": "API Benutzername:",
|
||||||
|
"user-placeholder": "API Benutzer",
|
||||||
|
"token-label": "API Passwort:",
|
||||||
|
"token-placeholder": "API Token",
|
||||||
|
"token-created-label": "API-Zugriff gewährt seit: ",
|
||||||
|
"button-disable-title": "Deaktivieren Sie die API. Dadurch wird der aktuelle Token ungültig.",
|
||||||
|
"button-disable-text": "API deaktivieren",
|
||||||
|
"button-enable-title": "Aktivieren Sie die API, dadurch wird ein neuer Token generiert.",
|
||||||
|
"button-enable-text": "API aktivieren",
|
||||||
|
"api-link": "API Dokumentation"
|
||||||
|
}
|
||||||
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"user-view": {
|
"user-view": {
|
||||||
"headline": "User Account:",
|
"headline": "User Account:",
|
||||||
|
@@ -37,6 +37,7 @@
|
|||||||
"users": "Users",
|
"users": "Users",
|
||||||
"lang": "Toggle Language",
|
"lang": "Toggle Language",
|
||||||
"profile": "My Profile",
|
"profile": "My Profile",
|
||||||
|
"settings": "Settings",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"logout": "Logout"
|
"logout": "Logout"
|
||||||
},
|
},
|
||||||
@@ -167,6 +168,26 @@
|
|||||||
"button-show-peer": "Show Peer",
|
"button-show-peer": "Show Peer",
|
||||||
"button-edit-peer": "Edit Peer"
|
"button-edit-peer": "Edit Peer"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"headline": "Settings",
|
||||||
|
"abstract": "Here you can change your personal settings.",
|
||||||
|
"api": {
|
||||||
|
"headline": "API Settings",
|
||||||
|
"abstract": "Here you can configure the RESTful API settings.",
|
||||||
|
"active-description": "The API is currently active for your user account. All API requests are authenticated with Basic Auth. Use the following credentials for authentication.",
|
||||||
|
"inactive-description": "The API is currently inactive. Press the button below to activate it.",
|
||||||
|
"user-label": "API Username:",
|
||||||
|
"user-placeholder": "The API user",
|
||||||
|
"token-label": "API Password:",
|
||||||
|
"token-placeholder": "The API token",
|
||||||
|
"token-created-label": "API access granted at: ",
|
||||||
|
"button-disable-title": "Disable API, this will invalidate the current token.",
|
||||||
|
"button-disable-text": "Disable API",
|
||||||
|
"button-enable-title": "Enable API, this will generate a new token.",
|
||||||
|
"button-enable-text": "Enable API",
|
||||||
|
"api-link": "API Documentation"
|
||||||
|
}
|
||||||
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"user-view": {
|
"user-view": {
|
||||||
"headline": "User Account:",
|
"headline": "User Account:",
|
||||||
@@ -177,8 +198,9 @@
|
|||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"firstname": "Firstname",
|
"firstname": "Firstname",
|
||||||
"lastname": "Lastname",
|
"lastname": "Lastname",
|
||||||
"phone": "Phone number",
|
"phone": "Phone Number",
|
||||||
"department": "Department",
|
"department": "Department",
|
||||||
|
"api-enabled": "API Access",
|
||||||
"disabled": "Account Disabled",
|
"disabled": "Account Disabled",
|
||||||
"locked": "Account Locked",
|
"locked": "Account Locked",
|
||||||
"no-peers": "User has no associated peers.",
|
"no-peers": "User has no associated peers.",
|
||||||
|
@@ -47,6 +47,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/ProfileView.vue')
|
component: () => import('../views/ProfileView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'settings',
|
||||||
|
// route level code-splitting
|
||||||
|
// this generates a separate chunk (About.[hash].js) for this route
|
||||||
|
// which is lazy-loaded when the route is visited.
|
||||||
|
component: () => import('../views/SettingsView.vue')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
linkActiveClass: "active",
|
linkActiveClass: "active",
|
||||||
|
@@ -116,6 +116,34 @@ export const profileStore = defineStore({
|
|||||||
this.stats = statsResponse.Stats
|
this.stats = statsResponse.Stats
|
||||||
this.statsEnabled = statsResponse.Enabled
|
this.statsEnabled = statsResponse.Enabled
|
||||||
},
|
},
|
||||||
|
async enableApi() {
|
||||||
|
this.fetching = true
|
||||||
|
let currentUser = authStore().user.Identifier
|
||||||
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
|
||||||
|
.then(this.setUser)
|
||||||
|
.catch(error => {
|
||||||
|
this.setPeers([])
|
||||||
|
console.log("Failed to activate API for ", currentUser, ": ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to activate API!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async disableApi() {
|
||||||
|
this.fetching = true
|
||||||
|
let currentUser = authStore().user.Identifier
|
||||||
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
|
||||||
|
.then(this.setUser)
|
||||||
|
.catch(error => {
|
||||||
|
this.setPeers([])
|
||||||
|
console.log("Failed to deactivate API for ", currentUser, ": ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to deactivate API!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
async LoadPeers() {
|
async LoadPeers() {
|
||||||
this.fetching = true
|
this.fetching = true
|
||||||
let currentUser = authStore().user.Identifier
|
let currentUser = authStore().user.Identifier
|
||||||
|
77
frontend/src/views/SettingsView.vue
Normal file
77
frontend/src/views/SettingsView.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<script setup>
|
||||||
|
import PeerViewModal from "../components/PeerViewModal.vue";
|
||||||
|
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { profileStore } from "@/stores/profile";
|
||||||
|
import PeerEditModal from "@/components/PeerEditModal.vue";
|
||||||
|
import { settingsStore } from "@/stores/settings";
|
||||||
|
import { humanFileSize } from "@/helpers/utils";
|
||||||
|
import {RouterLink} from "vue-router";
|
||||||
|
import {authStore} from "../stores/auth";
|
||||||
|
|
||||||
|
const profile = profileStore()
|
||||||
|
const settings = settingsStore()
|
||||||
|
const auth = authStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await profile.LoadUser()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>{{ $t('settings.headline') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="lead">{{ $t('settings.abstract') }}</p>
|
||||||
|
|
||||||
|
<div v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')">
|
||||||
|
<div class="bg-light p-5" v-if="profile.user.ApiToken">
|
||||||
|
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||||
|
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
<p>{{ $t('settings.api.active-description') }}</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('settings.api.user-label') }}</label>
|
||||||
|
<input v-model="profile.user.Identifier" class="form-control" :placeholder="$t('settings.api.user-placeholder')" type="text" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('settings.api.token-label') }}</label>
|
||||||
|
<input v-model="profile.user.ApiToken" class="form-control" :placeholder="$t('settings.api.token-placeholder')" type="text" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<p class="form-label mt-4">{{ $t('settings.api.token-created-label') }} {{profile.user.ApiTokenCreated}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col-6">
|
||||||
|
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-disable-title')" @click.prevent="profile.disableApi()" :disabled="profile.isFetching">
|
||||||
|
<i class="fa-solid fa-minus-circle"></i> {{ $t('settings.api.button-disable-text') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<a href="/api/v1/doc.html" target="_blank" :alt="$t('settings.api.api-link')">{{ $t('settings.api.api-link') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-light p-5" v-else>
|
||||||
|
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||||
|
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
<p>{{ $t('settings.api.inactive-description') }}</p>
|
||||||
|
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-enable-title')" @click.prevent="profile.enableApi()" :disabled="profile.isFetching">
|
||||||
|
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.api.button-enable-text') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
20
go.mod
20
go.mod
@@ -10,6 +10,7 @@ require (
|
|||||||
github.com/gin-gonic/gin v1.10.0
|
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.10
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/prometheus-community/pro-bing v0.5.0
|
github.com/prometheus-community/pro-bing v0.5.0
|
||||||
github.com/prometheus/client_golang v1.20.5
|
github.com/prometheus/client_golang v1.20.5
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
@@ -21,7 +22,7 @@ require (
|
|||||||
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.4
|
||||||
golang.org/x/crypto v0.31.0
|
golang.org/x/crypto v0.32.0
|
||||||
golang.org/x/oauth2 v0.25.0
|
golang.org/x/oauth2 v0.25.0
|
||||||
golang.org/x/sys v0.29.0
|
golang.org/x/sys v0.29.0
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
@@ -37,8 +38,8 @@ 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.6 // indirect
|
github.com/bytedance/sonic v1.12.7 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.1 // 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/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
@@ -56,14 +57,13 @@ require (
|
|||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // 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.23.0 // indirect
|
github.com/go-playground/validator/v10 v10.24.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-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/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.6.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/gorilla/sessions v1.4.0 // indirect
|
github.com/gorilla/sessions v1.4.0 // indirect
|
||||||
@@ -103,15 +103,15 @@ require (
|
|||||||
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/arch v0.13.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.34.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
golang.org/x/tools v0.28.0 // indirect
|
golang.org/x/tools v0.29.0 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||||
google.golang.org/protobuf v1.36.1 // indirect
|
google.golang.org/protobuf v1.36.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.61.6 // indirect
|
modernc.org/libc v1.61.7 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.8.1 // indirect
|
modernc.org/memory v1.8.1 // indirect
|
||||||
modernc.org/sqlite v1.34.4 // indirect
|
modernc.org/sqlite v1.34.4 // indirect
|
||||||
|
24
go.sum
24
go.sum
@@ -36,9 +36,13 @@ github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6
|
|||||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||||
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
|
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
|
||||||
github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||||
|
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
||||||
|
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.2 h1:jxAJuN9fOot/cyz5Q6dUuMJF5OqQ6+5GfA8FjjQ0R4o=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.2/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 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/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
@@ -98,6 +102,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
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.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||||
|
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
@@ -304,8 +310,12 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
|||||||
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 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
|
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
|
||||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||||
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||||
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.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=
|
||||||
@@ -332,6 +342,8 @@ 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 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -403,6 +415,8 @@ 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.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||||
|
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||||
|
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.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=
|
||||||
@@ -410,6 +424,8 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdI
|
|||||||
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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||||
|
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 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-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=
|
||||||
@@ -437,26 +453,34 @@ 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.2 h1:uektamHbSXU7egelXcyVpMaaAsrRH4/+uMKUQAQUdOw=
|
modernc.org/cc/v4 v4.24.2 h1:uektamHbSXU7egelXcyVpMaaAsrRH4/+uMKUQAQUdOw=
|
||||||
modernc.org/cc/v4 v4.24.2/go.mod h1:T1lKJZhXIi2VSqGBiB4LIbKs9NsKTbUXj4IDrmGqtTI=
|
modernc.org/cc/v4 v4.24.2/go.mod h1:T1lKJZhXIi2VSqGBiB4LIbKs9NsKTbUXj4IDrmGqtTI=
|
||||||
|
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||||
modernc.org/ccgo/v4 v4.23.5 h1:6uAwu8u3pnla3l/+UVUrDDO1HIGxHTYmFH6w+X9nsyw=
|
modernc.org/ccgo/v4 v4.23.5 h1:6uAwu8u3pnla3l/+UVUrDDO1HIGxHTYmFH6w+X9nsyw=
|
||||||
modernc.org/ccgo/v4 v4.23.5/go.mod h1:FogrWfBdzqLWm1ku6cfr4IzEFouq2fSAPf6aSAHdAJQ=
|
modernc.org/ccgo/v4 v4.23.5/go.mod h1:FogrWfBdzqLWm1ku6cfr4IzEFouq2fSAPf6aSAHdAJQ=
|
||||||
|
modernc.org/ccgo/v4 v4.23.10 h1:DnDZT/H6TtoJvQmVf7d8W+lVqEZpIJY/+0ENFh1LIHE=
|
||||||
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.0 h1:Tiw3pezQj7PfV8k4Dzyu/vhRHR2e92kOXtTFU8pbCl4=
|
modernc.org/gc/v2 v2.6.0 h1:Tiw3pezQj7PfV8k4Dzyu/vhRHR2e92kOXtTFU8pbCl4=
|
||||||
modernc.org/gc/v2 v2.6.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
modernc.org/gc/v2 v2.6.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
|
modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=
|
||||||
modernc.org/libc v1.61.6 h1:L2jW0wxHPCyHK0YSHaGaVlY0WxjpG/TTVdg6gRJOPqw=
|
modernc.org/libc v1.61.6 h1:L2jW0wxHPCyHK0YSHaGaVlY0WxjpG/TTVdg6gRJOPqw=
|
||||||
modernc.org/libc v1.61.6/go.mod h1:G+DzuaCcReUYYg4nNSfigIfTDCENdj9EByglvaRx53A=
|
modernc.org/libc v1.61.6/go.mod h1:G+DzuaCcReUYYg4nNSfigIfTDCENdj9EByglvaRx53A=
|
||||||
|
modernc.org/libc v1.61.7 h1:exz8rasFniviSgh3dH7QBnQHqYh9lolA5hVYfsiwkfo=
|
||||||
|
modernc.org/libc v1.61.7/go.mod h1:xspSrXRNVSfWfcfqgvZDVe/Hw5kv4FVC6IRfoms5v/0=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 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.8.1 h1:HS1HRg1jEohnuONobEq2WrLEhLyw8+J42yLFTnllm2A=
|
||||||
modernc.org/memory v1.8.1/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
modernc.org/memory v1.8.1/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
||||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
|
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
|
||||||
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
|
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
|
||||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
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=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
@@ -295,6 +295,30 @@ func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, err
|
|||||||
return interfaces, nil
|
return interfaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
|
*domain.InterfaceStatus,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if id == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats []domain.InterfaceStatus
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Where("identifier = ?", id).Find(&stats).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stats) == 0 {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
stat := stats[0]
|
||||||
|
|
||||||
|
return &stat, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) {
|
func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) {
|
||||||
var users []domain.Interface
|
var users []domain.Interface
|
||||||
|
|
||||||
@@ -698,6 +722,30 @@ func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domai
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
|
var users []domain.User
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Where("email = ?", email).Find(&users).Error
|
||||||
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) == 0 {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) > 1 {
|
||||||
|
return nil, fmt.Errorf("found multiple users with email %s: %w", email, domain.ErrNotUnique)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := users[0]
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"swagger": "2.0",
|
"swagger": "2.0",
|
||||||
"info": {
|
"info": {
|
||||||
"description": "WireGuard Portal API - a testing API endpoint",
|
"description": "WireGuard Portal API - UI Endpoints",
|
||||||
"title": "WireGuard Portal API",
|
"title": "WireGuard Portal SPA-UI API",
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "WireGuard Portal Developers",
|
"name": "WireGuard Portal Developers",
|
||||||
"url": "https://github.com/h44z/wg-portal"
|
"url": "https://github.com/h44z/wg-portal"
|
||||||
@@ -175,6 +175,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/config/settings": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Configuration"
|
||||||
|
],
|
||||||
|
"summary": "Get the frontend settings object.",
|
||||||
|
"operationId": "config_handleSettingsGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The JavaScript contents",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/csrf": {
|
"/csrf": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -499,6 +519,91 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/interface/{id}/apply-peer-defaults": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Interface"
|
||||||
|
],
|
||||||
|
"summary": "Apply all peer defaults to the available peers.",
|
||||||
|
"operationId": "interfaces_handleApplyPeerDefaultsPost",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The interface identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The interface data",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Interface"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if applying peer defaults was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/interface/{id}/save-config": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Interface"
|
||||||
|
],
|
||||||
|
"summary": "Save the interface configuration in wg-quick format to a file.",
|
||||||
|
"operationId": "interfaces_handleSaveConfigPost",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The interface identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if saving the configuration was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/now": {
|
"/now": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Nothing more to describe...",
|
"description": "Nothing more to describe...",
|
||||||
@@ -526,9 +631,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/peer/config-mail": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Peer"
|
||||||
|
],
|
||||||
|
"summary": "Send peer configuration via email.",
|
||||||
|
"operationId": "peers_handleEmailPost",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "The peer mail request data",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.PeerMailRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if mail sending was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/peer/config-qr/{id}": {
|
"/peer/config-qr/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
"image/png",
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -536,11 +682,20 @@
|
|||||||
],
|
],
|
||||||
"summary": "Get peer configuration as qr code.",
|
"summary": "Get peer configuration as qr code.",
|
||||||
"operationId": "peers_handleQrCodeGet",
|
"operationId": "peers_handleQrCodeGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The peer identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "file"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -568,6 +723,15 @@
|
|||||||
],
|
],
|
||||||
"summary": "Get peer configuration as string.",
|
"summary": "Get peer configuration as string.",
|
||||||
"operationId": "peers_handleConfigGet",
|
"operationId": "peers_handleConfigGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The peer identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@@ -634,6 +798,59 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/peer/iface/{iface}/multiplenew": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Peer"
|
||||||
|
],
|
||||||
|
"summary": "Create multiple new peers for the given interface.",
|
||||||
|
"operationId": "peers_handleCreateMultiplePost",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The interface identifier",
|
||||||
|
"name": "iface",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The peer creation request data",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.MultiPeerRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.Peer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/peer/iface/{iface}/new": {
|
"/peer/iface/{iface}/new": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -725,6 +942,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/peer/iface/{iface}/stats": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Peer"
|
||||||
|
],
|
||||||
|
"summary": "Get peer stats for the given interface.",
|
||||||
|
"operationId": "peers_handleStatsGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The interface identifier",
|
||||||
|
"name": "iface",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.PeerStats"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/peer/{id}": {
|
"/peer/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -1041,6 +1299,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/user/{id}/api/disable": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Disable the REST API for the given user.",
|
||||||
|
"operationId": "users_handleApiDisablePost",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/{id}/api/enable": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Enable the REST API for the given user.",
|
||||||
|
"operationId": "users_handleApiEnablePost",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/user/{id}/peers": {
|
"/user/{id}/peers": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -1061,6 +1383,44 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/{id}/stats": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Get peer stats for the given user.",
|
||||||
|
"operationId": "users_handleStatsGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.PeerStats"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -1072,6 +1432,53 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"model.ConfigOption-array_string": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Overridable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.ConfigOption-int": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Overridable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.ConfigOption-string": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Overridable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.ConfigOption-uint32": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Overridable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model.Error": {
|
"model.Error": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1083,25 +1490,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"model.Int32ConfigOption": {
|
"model.ExpiryDate": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"Overridable": {
|
"time.Time": {
|
||||||
"type": "boolean"
|
"type": "string"
|
||||||
},
|
|
||||||
"Value": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model.IntConfigOption": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"Overridable": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"Value": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1290,6 +1683,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"model.MultiPeerRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Identifiers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Suffix": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model.Peer": {
|
"model.Peer": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1304,7 +1711,7 @@
|
|||||||
"description": "all allowed ip subnets, comma seperated",
|
"description": "all allowed ip subnets, comma seperated",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringSliceConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-array_string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1328,7 +1735,7 @@
|
|||||||
"description": "the dns server that should be set if the interface is up, comma separated",
|
"description": "the dns server that should be set if the interface is up, comma separated",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringSliceConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-array_string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1336,7 +1743,7 @@
|
|||||||
"description": "the dns search option string that should be set if the interface is up, will be appended to DnsStr",
|
"description": "the dns search option string that should be set if the interface is up, will be appended to DnsStr",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringSliceConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-array_string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1344,7 +1751,7 @@
|
|||||||
"description": "the endpoint address",
|
"description": "the endpoint address",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1352,13 +1759,17 @@
|
|||||||
"description": "the endpoint public key",
|
"description": "the endpoint public key",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ExpiresAt": {
|
"ExpiresAt": {
|
||||||
"description": "expiry dates for peers",
|
"description": "expiry dates for peers",
|
||||||
"type": "string"
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/model.ExpiryDate"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"ExtraAllowedIPs": {
|
"ExtraAllowedIPs": {
|
||||||
"description": "all allowed ip subnets on the server side, comma seperated",
|
"description": "all allowed ip subnets on the server side, comma seperated",
|
||||||
@@ -1371,7 +1782,7 @@
|
|||||||
"description": "a firewall mark",
|
"description": "a firewall mark",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.Int32ConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-uint32"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1392,7 +1803,7 @@
|
|||||||
"description": "the device MTU",
|
"description": "the device MTU",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.IntConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-int"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1404,7 +1815,7 @@
|
|||||||
"description": "the persistent keep-alive interval",
|
"description": "the persistent keep-alive interval",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.IntConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-int"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1412,7 +1823,7 @@
|
|||||||
"description": "action that is executed after the device is down",
|
"description": "action that is executed after the device is down",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1420,7 +1831,7 @@
|
|||||||
"description": "action that is executed after the device is up",
|
"description": "action that is executed after the device is up",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1428,7 +1839,7 @@
|
|||||||
"description": "action that is executed before the device is down",
|
"description": "action that is executed before the device is down",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1436,7 +1847,7 @@
|
|||||||
"description": "action that is executed before the device is up",
|
"description": "action that is executed before the device is up",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1458,7 +1869,7 @@
|
|||||||
"description": "the routing table",
|
"description": "the routing table",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1468,6 +1879,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"model.PeerMailRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Identifiers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LinkOnly": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.PeerStatData": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"BytesReceived": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"BytesTransmitted": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"EndpointAddress": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"IsConnected": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"IsPingable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"LastHandshake": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"LastPing": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"LastSessionStart": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.PeerStats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Enabled": {
|
||||||
|
"description": "peer stats tracking enabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"Stats": {
|
||||||
|
"description": "stats, map key = Peer identifier",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/definitions/model.PeerStatData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model.SessionInfo": {
|
"model.SessionInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1491,34 +1962,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"model.StringConfigOption": {
|
"model.Settings": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"Overridable": {
|
"ApiAdminOnly": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"Value": {
|
"MailLinkOnly": {
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model.StringSliceConfigOption": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"Overridable": {
|
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"Value": {
|
"PersistentConfigSupported": {
|
||||||
"type": "array",
|
"type": "boolean"
|
||||||
"items": {
|
},
|
||||||
"type": "string"
|
"SelfProvisioning": {
|
||||||
}
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"model.User": {
|
"model.User": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"ApiEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"ApiToken": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ApiTokenCreated": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"Department": {
|
"Department": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1545,6 +2017,14 @@
|
|||||||
"Lastname": {
|
"Lastname": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"Locked": {
|
||||||
|
"description": "if this field is set, the user is locked",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"LockedReason": {
|
||||||
|
"description": "the reason why the user has been locked",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"Notes": {
|
"Notes": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@@ -1,5 +1,35 @@
|
|||||||
basePath: /api/v0
|
basePath: /api/v0
|
||||||
definitions:
|
definitions:
|
||||||
|
model.ConfigOption-array_string:
|
||||||
|
properties:
|
||||||
|
Overridable:
|
||||||
|
type: boolean
|
||||||
|
Value:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
model.ConfigOption-int:
|
||||||
|
properties:
|
||||||
|
Overridable:
|
||||||
|
type: boolean
|
||||||
|
Value:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
model.ConfigOption-string:
|
||||||
|
properties:
|
||||||
|
Overridable:
|
||||||
|
type: boolean
|
||||||
|
Value:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
model.ConfigOption-uint32:
|
||||||
|
properties:
|
||||||
|
Overridable:
|
||||||
|
type: boolean
|
||||||
|
Value:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
model.Error:
|
model.Error:
|
||||||
properties:
|
properties:
|
||||||
Code:
|
Code:
|
||||||
@@ -7,19 +37,10 @@ definitions:
|
|||||||
Message:
|
Message:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
model.Int32ConfigOption:
|
model.ExpiryDate:
|
||||||
properties:
|
properties:
|
||||||
Overridable:
|
time.Time:
|
||||||
type: boolean
|
type: string
|
||||||
Value:
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
model.IntConfigOption:
|
|
||||||
properties:
|
|
||||||
Overridable:
|
|
||||||
type: boolean
|
|
||||||
Value:
|
|
||||||
type: integer
|
|
||||||
type: object
|
type: object
|
||||||
model.Interface:
|
model.Interface:
|
||||||
properties:
|
properties:
|
||||||
@@ -160,6 +181,15 @@ definitions:
|
|||||||
example: /auth/google/login
|
example: /auth/google/login
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
model.MultiPeerRequest:
|
||||||
|
properties:
|
||||||
|
Identifiers:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
Suffix:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
model.Peer:
|
model.Peer:
|
||||||
properties:
|
properties:
|
||||||
Addresses:
|
Addresses:
|
||||||
@@ -169,7 +199,7 @@ definitions:
|
|||||||
type: array
|
type: array
|
||||||
AllowedIPs:
|
AllowedIPs:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringSliceConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-array_string'
|
||||||
description: all allowed ip subnets, comma seperated
|
description: all allowed ip subnets, comma seperated
|
||||||
CheckAliveAddress:
|
CheckAliveAddress:
|
||||||
description: optional ip address or DNS name that is used for ping checks
|
description: optional ip address or DNS name that is used for ping checks
|
||||||
@@ -185,25 +215,26 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
Dns:
|
Dns:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringSliceConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-array_string'
|
||||||
description: the dns server that should be set if the interface is up, comma
|
description: the dns server that should be set if the interface is up, comma
|
||||||
separated
|
separated
|
||||||
DnsSearch:
|
DnsSearch:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringSliceConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-array_string'
|
||||||
description: the dns search option string that should be set if the interface
|
description: the dns search option string that should be set if the interface
|
||||||
is up, will be appended to DnsStr
|
is up, will be appended to DnsStr
|
||||||
Endpoint:
|
Endpoint:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: the endpoint address
|
description: the endpoint address
|
||||||
EndpointPublicKey:
|
EndpointPublicKey:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: the endpoint public key
|
description: the endpoint public key
|
||||||
ExpiresAt:
|
ExpiresAt:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/model.ExpiryDate'
|
||||||
description: expiry dates for peers
|
description: expiry dates for peers
|
||||||
type: string
|
|
||||||
ExtraAllowedIPs:
|
ExtraAllowedIPs:
|
||||||
description: all allowed ip subnets on the server side, comma seperated
|
description: all allowed ip subnets on the server side, comma seperated
|
||||||
items:
|
items:
|
||||||
@@ -211,7 +242,7 @@ definitions:
|
|||||||
type: array
|
type: array
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.Int32ConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-uint32'
|
||||||
description: a firewall mark
|
description: a firewall mark
|
||||||
Identifier:
|
Identifier:
|
||||||
description: peer unique identifier
|
description: peer unique identifier
|
||||||
@@ -225,30 +256,30 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
Mtu:
|
Mtu:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.IntConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-int'
|
||||||
description: the device MTU
|
description: the device MTU
|
||||||
Notes:
|
Notes:
|
||||||
description: a note field for peers
|
description: a note field for peers
|
||||||
type: string
|
type: string
|
||||||
PersistentKeepalive:
|
PersistentKeepalive:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.IntConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-int'
|
||||||
description: the persistent keep-alive interval
|
description: the persistent keep-alive interval
|
||||||
PostDown:
|
PostDown:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: action that is executed after the device is down
|
description: action that is executed after the device is down
|
||||||
PostUp:
|
PostUp:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: action that is executed after the device is up
|
description: action that is executed after the device is up
|
||||||
PreDown:
|
PreDown:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: action that is executed before the device is down
|
description: action that is executed before the device is down
|
||||||
PreUp:
|
PreUp:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: action that is executed before the device is up
|
description: action that is executed before the device is up
|
||||||
PresharedKey:
|
PresharedKey:
|
||||||
description: the pre-shared Key of the peer
|
description: the pre-shared Key of the peer
|
||||||
@@ -263,12 +294,52 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
RoutingTable:
|
RoutingTable:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: the routing table
|
description: the routing table
|
||||||
UserIdentifier:
|
UserIdentifier:
|
||||||
description: the owner
|
description: the owner
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
model.PeerMailRequest:
|
||||||
|
properties:
|
||||||
|
Identifiers:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
LinkOnly:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
model.PeerStatData:
|
||||||
|
properties:
|
||||||
|
BytesReceived:
|
||||||
|
type: integer
|
||||||
|
BytesTransmitted:
|
||||||
|
type: integer
|
||||||
|
EndpointAddress:
|
||||||
|
type: string
|
||||||
|
IsConnected:
|
||||||
|
type: boolean
|
||||||
|
IsPingable:
|
||||||
|
type: boolean
|
||||||
|
LastHandshake:
|
||||||
|
type: string
|
||||||
|
LastPing:
|
||||||
|
type: string
|
||||||
|
LastSessionStart:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
model.PeerStats:
|
||||||
|
properties:
|
||||||
|
Enabled:
|
||||||
|
description: peer stats tracking enabled
|
||||||
|
example: true
|
||||||
|
type: boolean
|
||||||
|
Stats:
|
||||||
|
additionalProperties:
|
||||||
|
$ref: '#/definitions/model.PeerStatData'
|
||||||
|
description: stats, map key = Peer identifier
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
model.SessionInfo:
|
model.SessionInfo:
|
||||||
properties:
|
properties:
|
||||||
IsAdmin:
|
IsAdmin:
|
||||||
@@ -284,24 +355,25 @@ definitions:
|
|||||||
UserLastname:
|
UserLastname:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
model.StringConfigOption:
|
model.Settings:
|
||||||
properties:
|
properties:
|
||||||
Overridable:
|
ApiAdminOnly:
|
||||||
type: boolean
|
type: boolean
|
||||||
Value:
|
MailLinkOnly:
|
||||||
type: string
|
type: boolean
|
||||||
type: object
|
PersistentConfigSupported:
|
||||||
model.StringSliceConfigOption:
|
type: boolean
|
||||||
properties:
|
SelfProvisioning:
|
||||||
Overridable:
|
|
||||||
type: boolean
|
type: boolean
|
||||||
Value:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
type: object
|
type: object
|
||||||
model.User:
|
model.User:
|
||||||
properties:
|
properties:
|
||||||
|
ApiEnabled:
|
||||||
|
type: boolean
|
||||||
|
ApiToken:
|
||||||
|
type: string
|
||||||
|
ApiTokenCreated:
|
||||||
|
type: string
|
||||||
Department:
|
Department:
|
||||||
type: string
|
type: string
|
||||||
Disabled:
|
Disabled:
|
||||||
@@ -320,6 +392,12 @@ definitions:
|
|||||||
type: boolean
|
type: boolean
|
||||||
Lastname:
|
Lastname:
|
||||||
type: string
|
type: string
|
||||||
|
Locked:
|
||||||
|
description: if this field is set, the user is locked
|
||||||
|
type: boolean
|
||||||
|
LockedReason:
|
||||||
|
description: the reason why the user has been locked
|
||||||
|
type: string
|
||||||
Notes:
|
Notes:
|
||||||
type: string
|
type: string
|
||||||
Password:
|
Password:
|
||||||
@@ -337,8 +415,8 @@ info:
|
|||||||
contact:
|
contact:
|
||||||
name: WireGuard Portal Developers
|
name: WireGuard Portal Developers
|
||||||
url: https://github.com/h44z/wg-portal
|
url: https://github.com/h44z/wg-portal
|
||||||
description: WireGuard Portal API - a testing API endpoint
|
description: WireGuard Portal API - UI Endpoints
|
||||||
title: WireGuard Portal API
|
title: WireGuard Portal SPA-UI API
|
||||||
version: "0.0"
|
version: "0.0"
|
||||||
paths:
|
paths:
|
||||||
/auth/{provider}/callback:
|
/auth/{provider}/callback:
|
||||||
@@ -448,6 +526,19 @@ paths:
|
|||||||
summary: Get the dynamic frontend configuration javascript.
|
summary: Get the dynamic frontend configuration javascript.
|
||||||
tags:
|
tags:
|
||||||
- Configuration
|
- Configuration
|
||||||
|
/config/settings:
|
||||||
|
get:
|
||||||
|
operationId: config_handleSettingsGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The JavaScript contents
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Get the frontend settings object.
|
||||||
|
tags:
|
||||||
|
- Configuration
|
||||||
/csrf:
|
/csrf:
|
||||||
get:
|
get:
|
||||||
operationId: base_handleCsrfGet
|
operationId: base_handleCsrfGet
|
||||||
@@ -536,6 +627,62 @@ paths:
|
|||||||
summary: Update the interface record.
|
summary: Update the interface record.
|
||||||
tags:
|
tags:
|
||||||
- Interface
|
- Interface
|
||||||
|
/interface/{id}/apply-peer-defaults:
|
||||||
|
post:
|
||||||
|
operationId: interfaces_handleApplyPeerDefaultsPost
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: The interface data
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Interface'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if applying peer defaults was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Apply all peer defaults to the available peers.
|
||||||
|
tags:
|
||||||
|
- Interface
|
||||||
|
/interface/{id}/save-config:
|
||||||
|
post:
|
||||||
|
operationId: interfaces_handleSaveConfigPost
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if saving the configuration was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Save the interface configuration in wg-quick format to a file.
|
||||||
|
tags:
|
||||||
|
- Interface
|
||||||
/interface/all:
|
/interface/all:
|
||||||
get:
|
get:
|
||||||
operationId: interfaces_handleAllGet
|
operationId: interfaces_handleAllGet
|
||||||
@@ -762,16 +909,49 @@ paths:
|
|||||||
summary: Update the given peer record.
|
summary: Update the given peer record.
|
||||||
tags:
|
tags:
|
||||||
- Peer
|
- Peer
|
||||||
|
/peer/config-mail:
|
||||||
|
post:
|
||||||
|
operationId: peers_handleEmailPost
|
||||||
|
parameters:
|
||||||
|
- description: The peer mail request data
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.PeerMailRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if mail sending was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Send peer configuration via email.
|
||||||
|
tags:
|
||||||
|
- Peer
|
||||||
/peer/config-qr/{id}:
|
/peer/config-qr/{id}:
|
||||||
get:
|
get:
|
||||||
operationId: peers_handleQrCodeGet
|
operationId: peers_handleQrCodeGet
|
||||||
|
parameters:
|
||||||
|
- description: The peer identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
|
- image/png
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: file
|
||||||
"400":
|
"400":
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
@@ -786,6 +966,12 @@ paths:
|
|||||||
/peer/config/{id}:
|
/peer/config/{id}:
|
||||||
get:
|
get:
|
||||||
operationId: peers_handleConfigGet
|
operationId: peers_handleConfigGet
|
||||||
|
parameters:
|
||||||
|
- description: The peer identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -833,6 +1019,41 @@ paths:
|
|||||||
summary: Get peers for the given interface.
|
summary: Get peers for the given interface.
|
||||||
tags:
|
tags:
|
||||||
- Peer
|
- Peer
|
||||||
|
/peer/iface/{iface}/multiplenew:
|
||||||
|
post:
|
||||||
|
operationId: peers_handleCreateMultiplePost
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier
|
||||||
|
in: path
|
||||||
|
name: iface
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: The peer creation request data
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.MultiPeerRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.Peer'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Create multiple new peers for the given interface.
|
||||||
|
tags:
|
||||||
|
- Peer
|
||||||
/peer/iface/{iface}/new:
|
/peer/iface/{iface}/new:
|
||||||
post:
|
post:
|
||||||
operationId: peers_handleCreatePost
|
operationId: peers_handleCreatePost
|
||||||
@@ -893,6 +1114,33 @@ paths:
|
|||||||
summary: Prepare a new peer for the given interface.
|
summary: Prepare a new peer for the given interface.
|
||||||
tags:
|
tags:
|
||||||
- Peer
|
- Peer
|
||||||
|
/peer/iface/{iface}/stats:
|
||||||
|
get:
|
||||||
|
operationId: peers_handleStatsGet
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier
|
||||||
|
in: path
|
||||||
|
name: iface
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.PeerStats'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Get peer stats for the given interface.
|
||||||
|
tags:
|
||||||
|
- Peer
|
||||||
/user/{id}:
|
/user/{id}:
|
||||||
delete:
|
delete:
|
||||||
operationId: users_handleDelete
|
operationId: users_handleDelete
|
||||||
@@ -972,6 +1220,48 @@ paths:
|
|||||||
summary: Update the user record.
|
summary: Update the user record.
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
|
/user/{id}/api/disable:
|
||||||
|
post:
|
||||||
|
operationId: users_handleApiDisablePost
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.User'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Disable the REST API for the given user.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
|
/user/{id}/api/enable:
|
||||||
|
post:
|
||||||
|
operationId: users_handleApiEnablePost
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.User'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Enable the REST API for the given user.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
/user/{id}/peers:
|
/user/{id}/peers:
|
||||||
get:
|
get:
|
||||||
operationId: users_handlePeersGet
|
operationId: users_handlePeersGet
|
||||||
@@ -984,6 +1274,10 @@ paths:
|
|||||||
items:
|
items:
|
||||||
$ref: '#/definitions/model.Peer'
|
$ref: '#/definitions/model.Peer'
|
||||||
type: array
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
@@ -991,6 +1285,27 @@ paths:
|
|||||||
summary: Get peers for the given user.
|
summary: Get peers for the given user.
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
|
/user/{id}/stats:
|
||||||
|
get:
|
||||||
|
operationId: users_handleStatsGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.PeerStats'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Get peer stats for the given user.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
/user/all:
|
/user/all:
|
||||||
get:
|
get:
|
||||||
operationId: users_handleAllGet
|
operationId: users_handleAllGet
|
||||||
|
2203
internal/app/api/core/assets/doc/v1_swagger.json
Normal file
2203
internal/app/api/core/assets/doc/v1_swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
1546
internal/app/api/core/assets/doc/v1_swagger.yaml
Normal file
1546
internal/app/api/core/assets/doc/v1_swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
3917
internal/app/api/core/assets/js/rapidoc-min.js
vendored
3917
internal/app/api/core/assets/js/rapidoc-min.js
vendored
File diff suppressed because one or more lines are too long
@@ -8,10 +8,16 @@
|
|||||||
<rapi-doc
|
<rapi-doc
|
||||||
spec-url="{{ $.ApiSpecUrl }}"
|
spec-url="{{ $.ApiSpecUrl }}"
|
||||||
theme="dark"
|
theme="dark"
|
||||||
|
render-style="focused"
|
||||||
allow-server-selection="false"
|
allow-server-selection="false"
|
||||||
allow-authentication="false"
|
allow-authentication="true"
|
||||||
load-fonts="false"
|
load-fonts="false"
|
||||||
|
schema-style="table"
|
||||||
schema-expand-level="1"
|
schema-expand-level="1"
|
||||||
|
default-schema-tab="model"
|
||||||
|
fill-request-fields-with-example="true"
|
||||||
|
show-method-in-nav-bar="as-colored-block"
|
||||||
|
show-components="true"
|
||||||
allow-spec-url-load="false"
|
allow-spec-url-load="false"
|
||||||
allow-spec-file-load="false"
|
allow-spec-file-load="false"
|
||||||
allow-spec-file-download="true"
|
allow-spec-file-download="true"
|
||||||
|
@@ -88,6 +88,8 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server,
|
|||||||
s.server.StaticFS("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
s.server.StaticFS("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes
|
||||||
|
s.server.UseRawPath = true
|
||||||
|
s.server.UnescapePathValues = true
|
||||||
s.setupRoutes(endpoints...)
|
s.setupRoutes(endpoints...)
|
||||||
s.setupFrontendRoutes()
|
s.setupFrontendRoutes()
|
||||||
|
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-contrib/sessions/memstore"
|
"github.com/gin-contrib/sessions/memstore"
|
||||||
@@ -10,8 +13,6 @@ import (
|
|||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
csrf "github.com/utrack/gin-csrf"
|
csrf "github.com/utrack/gin-csrf"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type handler interface {
|
type handler interface {
|
||||||
@@ -20,12 +21,12 @@ type handler interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// To compile the API documentation use the
|
// To compile the API documentation use the
|
||||||
// build_tool
|
// api_build_tool
|
||||||
// command that can be found in the $PROJECT_ROOT/internal/ports/api/build_tool directory.
|
// command that can be found in the $PROJECT_ROOT/cmd/api_build_tool directory.
|
||||||
|
|
||||||
// @title WireGuard Portal API
|
// @title WireGuard Portal SPA-UI API
|
||||||
// @version 0.0
|
// @version 0.0
|
||||||
// @description WireGuard Portal API - a testing API endpoint
|
// @description WireGuard Portal API - UI Endpoints
|
||||||
|
|
||||||
// @contact.name WireGuard Portal Developers
|
// @contact.name WireGuard Portal Developers
|
||||||
// @contact.url https://github.com/h44z/wg-portal
|
// @contact.url https://github.com/h44z/wg-portal
|
||||||
|
@@ -4,13 +4,14 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
|
||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed frontend_config.js.gotpl
|
//go:embed frontend_config.js.gotpl
|
||||||
@@ -63,7 +64,8 @@ func (e configEndpoint) handleConfigJsGet() gin.HandlerFunc {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
||||||
}
|
}
|
||||||
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host, port) // override if request comes from frontend started with npm run dev
|
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host,
|
||||||
|
port) // override if request comes from frontend started with npm run dev
|
||||||
}
|
}
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
err := e.tpl.ExecuteTemplate(buf, "frontend_config.js.gotpl", gin.H{
|
err := e.tpl.ExecuteTemplate(buf, "frontend_config.js.gotpl", gin.H{
|
||||||
@@ -96,6 +98,7 @@ func (e configEndpoint) handleSettingsGet() gin.HandlerFunc {
|
|||||||
MailLinkOnly: e.app.Config.Mail.LinkOnly,
|
MailLinkOnly: e.app.Config.Mail.LinkOnly,
|
||||||
PersistentConfigSupported: e.app.Config.Advanced.ConfigStoragePath != "",
|
PersistentConfigSupported: e.app.Config.Advanced.ConfigStoragePath != "",
|
||||||
SelfProvisioning: e.app.Config.Core.SelfProvisioningAllowed,
|
SelfProvisioning: e.app.Config.Core.SelfProvisioningAllowed,
|
||||||
|
ApiAdminOnly: e.app.Config.Advanced.ApiAdminOnly,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type peerEndpoint struct {
|
type peerEndpoint struct {
|
||||||
@@ -57,7 +58,8 @@ func (e peerEndpoint) handleAllGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
_, peers, err := e.app.GetInterfaceAndPeers(ctx, domain.InterfaceIdentifier(interfaceId))
|
_, peers, err := e.app.GetInterfaceAndPeers(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +90,8 @@ func (e peerEndpoint) handleSingleGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
peer, err := e.app.GetPeer(ctx, domain.PeerIdentifier(peerId))
|
peer, err := e.app.GetPeer(ctx, domain.PeerIdentifier(peerId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +122,8 @@ func (e peerEndpoint) handlePrepareGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
peer, err := e.app.PreparePeer(ctx, domain.InterfaceIdentifier(interfaceId))
|
peer, err := e.app.PreparePeer(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +167,8 @@ func (e peerEndpoint) handleCreatePost() gin.HandlerFunc {
|
|||||||
|
|
||||||
newPeer, err := e.app.CreatePeer(ctx, model.NewDomainPeer(&p))
|
newPeer, err := e.app.CreatePeer(ctx, model.NewDomainPeer(&p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,9 +205,11 @@ func (e peerEndpoint) handleCreateMultiplePost() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newPeers, err := e.app.CreateMultiplePeers(ctx, domain.InterfaceIdentifier(interfaceId), model.NewDomainPeerCreationRequest(&req))
|
newPeers, err := e.app.CreateMultiplePeers(ctx, domain.InterfaceIdentifier(interfaceId),
|
||||||
|
model.NewDomainPeerCreationRequest(&req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +253,8 @@ func (e peerEndpoint) handleUpdatePut() gin.HandlerFunc {
|
|||||||
|
|
||||||
updatedPeer, err := e.app.UpdatePeer(ctx, model.NewDomainPeer(&p))
|
updatedPeer, err := e.app.UpdatePeer(ctx, model.NewDomainPeer(&p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +285,8 @@ func (e peerEndpoint) handleDelete() gin.HandlerFunc {
|
|||||||
|
|
||||||
err := e.app.DeletePeer(ctx, domain.PeerIdentifier(id))
|
err := e.app.DeletePeer(ctx, domain.PeerIdentifier(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,9 +342,10 @@ func (e peerEndpoint) handleConfigGet() gin.HandlerFunc {
|
|||||||
// @ID peers_handleQrCodeGet
|
// @ID peers_handleQrCodeGet
|
||||||
// @Tags Peer
|
// @Tags Peer
|
||||||
// @Summary Get peer configuration as qr code.
|
// @Summary Get peer configuration as qr code.
|
||||||
|
// @Produce png
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "The peer identifier"
|
// @Param id path string true "The peer identifier"
|
||||||
// @Success 200 {object} string
|
// @Success 200 {file} binary
|
||||||
// @Failure 400 {object} model.Error
|
// @Failure 400 {object} model.Error
|
||||||
// @Failure 500 {object} model.Error
|
// @Failure 500 {object} model.Error
|
||||||
// @Router /peer/config-qr/{id} [get]
|
// @Router /peer/config-qr/{id} [get]
|
||||||
@@ -403,7 +413,8 @@ func (e peerEndpoint) handleEmailPost() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
err = e.app.SendPeerEmail(ctx, req.LinkOnly, peerIds...)
|
err = e.app.SendPeerEmail(ctx, req.LinkOnly, peerIds...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +445,8 @@ func (e peerEndpoint) handleStatsGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
stats, err := e.app.GetPeerStats(ctx, domain.InterfaceIdentifier(interfaceId))
|
stats, err := e.app.GetPeerStats(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type userEndpoint struct {
|
type userEndpoint struct {
|
||||||
@@ -27,6 +28,8 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
|
|||||||
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
|
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
|
||||||
apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
|
apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
|
||||||
|
apiGroup.POST("/:id/api/enable", e.authenticator.UserIdMatch("id"), e.handleApiEnablePost())
|
||||||
|
apiGroup.POST("/:id/api/disable", e.authenticator.UserIdMatch("id"), e.handleApiDisablePost())
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAllGet returns a gorm handler function.
|
// handleAllGet returns a gorm handler function.
|
||||||
@@ -44,7 +47,8 @@ func (e userEndpoint) handleAllGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
users, err := e.app.GetAllUsers(ctx)
|
users, err := e.app.GetAllUsers(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,11 +78,12 @@ func (e userEndpoint) handleSingleGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
user, err := e.app.GetUser(ctx, domain.UserIdentifier(id))
|
user, err := e.app.GetUser(ctx, domain.UserIdentifier(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewUser(user))
|
c.JSON(http.StatusOK, model.NewUser(user, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,11 +123,12 @@ func (e userEndpoint) handleUpdatePut() gin.HandlerFunc {
|
|||||||
|
|
||||||
updateUser, err := e.app.UpdateUser(ctx, model.NewDomainUser(&user))
|
updateUser, err := e.app.UpdateUser(ctx, model.NewDomainUser(&user))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewUser(updateUser))
|
c.JSON(http.StatusOK, model.NewUser(updateUser, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,11 +156,12 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
|
|||||||
|
|
||||||
newUser, err := e.app.CreateUser(ctx, model.NewDomainUser(&user))
|
newUser, err := e.app.CreateUser(ctx, model.NewDomainUser(&user))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewUser(newUser))
|
c.JSON(http.StatusOK, model.NewUser(newUser, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,13 +181,15 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
interfaceId := Base64UrlDecode(c.Param("id"))
|
interfaceId := Base64UrlDecode(c.Param("id"))
|
||||||
if interfaceId == "" {
|
if interfaceId == "" {
|
||||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(interfaceId))
|
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,13 +213,15 @@ func (e userEndpoint) handleStatsGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
userId := Base64UrlDecode(c.Param("id"))
|
userId := Base64UrlDecode(c.Param("id"))
|
||||||
if userId == "" {
|
if userId == "" {
|
||||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stats, err := e.app.GetUserPeerStats(ctx, domain.UserIdentifier(userId))
|
stats, err := e.app.GetUserPeerStats(ctx, domain.UserIdentifier(userId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,10 +252,75 @@ func (e userEndpoint) handleDelete() gin.HandlerFunc {
|
|||||||
|
|
||||||
err := e.app.DeleteUser(ctx, domain.UserIdentifier(id))
|
err := e.app.DeleteUser(ctx, domain.UserIdentifier(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleApiEnablePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleApiEnablePost
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Enable the REST API for the given user.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} model.User
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/{id}/api/enable [post]
|
||||||
|
func (e userEndpoint) handleApiEnablePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
userId := Base64UrlDecode(c.Param("id"))
|
||||||
|
if userId == "" {
|
||||||
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := e.app.ActivateApi(ctx, domain.UserIdentifier(userId))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.NewUser(user, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleApiDisablePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleApiDisablePost
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Disable the REST API for the given user.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} model.User
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/{id}/api/disable [post]
|
||||||
|
func (e userEndpoint) handleApiDisablePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
userId := Base64UrlDecode(c.Param("id"))
|
||||||
|
if userId == "" {
|
||||||
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := e.app.DeactivateApi(ctx, domain.UserIdentifier(userId))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.NewUser(user, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -9,4 +9,5 @@ type Settings struct {
|
|||||||
MailLinkOnly bool `json:"MailLinkOnly"`
|
MailLinkOnly bool `json:"MailLinkOnly"`
|
||||||
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
||||||
SelfProvisioning bool `json:"SelfProvisioning"`
|
SelfProvisioning bool `json:"SelfProvisioning"`
|
||||||
|
ApiAdminOnly bool `json:"ApiAdminOnly"`
|
||||||
}
|
}
|
||||||
|
@@ -25,37 +25,50 @@ type User struct {
|
|||||||
Locked bool `json:"Locked"` // if this field is set, the user is locked
|
Locked bool `json:"Locked"` // if this field is set, the user is locked
|
||||||
LockedReason string `json:"LockedReason"` // the reason why the user has been locked
|
LockedReason string `json:"LockedReason"` // the reason why the user has been locked
|
||||||
|
|
||||||
|
ApiToken string `json:"ApiToken"`
|
||||||
|
ApiTokenCreated *time.Time `json:"ApiTokenCreated,omitempty"`
|
||||||
|
ApiEnabled bool `json:"ApiEnabled"`
|
||||||
|
|
||||||
// Calculated
|
// Calculated
|
||||||
|
|
||||||
PeerCount int `json:"PeerCount"`
|
PeerCount int `json:"PeerCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser(src *domain.User) *User {
|
func NewUser(src *domain.User, exposeCreds bool) *User {
|
||||||
return &User{
|
u := &User{
|
||||||
Identifier: string(src.Identifier),
|
Identifier: string(src.Identifier),
|
||||||
Email: src.Email,
|
Email: src.Email,
|
||||||
Source: string(src.Source),
|
Source: string(src.Source),
|
||||||
ProviderName: src.ProviderName,
|
ProviderName: src.ProviderName,
|
||||||
IsAdmin: src.IsAdmin,
|
IsAdmin: src.IsAdmin,
|
||||||
Firstname: src.Firstname,
|
Firstname: src.Firstname,
|
||||||
Lastname: src.Lastname,
|
Lastname: src.Lastname,
|
||||||
Phone: src.Phone,
|
Phone: src.Phone,
|
||||||
Department: src.Department,
|
Department: src.Department,
|
||||||
Notes: src.Notes,
|
Notes: src.Notes,
|
||||||
Password: "", // never fill password
|
Password: "", // never fill password
|
||||||
Disabled: src.IsDisabled(),
|
Disabled: src.IsDisabled(),
|
||||||
DisabledReason: src.DisabledReason,
|
DisabledReason: src.DisabledReason,
|
||||||
Locked: src.IsLocked(),
|
Locked: src.IsLocked(),
|
||||||
LockedReason: src.LockedReason,
|
LockedReason: src.LockedReason,
|
||||||
|
ApiToken: "", // by default, do not expose API token
|
||||||
|
ApiTokenCreated: src.ApiTokenCreated,
|
||||||
|
ApiEnabled: src.IsApiEnabled(),
|
||||||
|
|
||||||
PeerCount: src.LinkedPeerCount,
|
PeerCount: src.LinkedPeerCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if exposeCreds {
|
||||||
|
u.ApiToken = src.ApiToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUsers(src []domain.User) []User {
|
func NewUsers(src []domain.User) []User {
|
||||||
results := make([]User, len(src))
|
results := make([]User, len(src))
|
||||||
for i := range src {
|
for i := range src {
|
||||||
results[i] = *NewUser(&src[i])
|
results[i] = *NewUser(&src[i], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
109
internal/app/api/v1/backend/interface_service.go
Normal file
109
internal/app/api/v1/backend/interface_service.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InterfaceServiceInterfaceManagerRepo interface {
|
||||||
|
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||||
|
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
|
||||||
|
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||||
|
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
interfaces InterfaceServiceInterfaceManagerRepo
|
||||||
|
users PeerServiceUserManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterfaceService(cfg *config.Config, interfaces InterfaceServiceInterfaceManagerRepo) *InterfaceService {
|
||||||
|
return &InterfaceService{
|
||||||
|
cfg: cfg,
|
||||||
|
interfaces: interfaces,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) GetAll(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces, interfacePeers, err := s.interfaces.GetAllInterfacesAndPeers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces, interfacePeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) GetById(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
|
*domain.Interface,
|
||||||
|
[]domain.Peer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceData, interfacePeers, err := s.interfaces.GetInterfaceAndPeers(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaceData, interfacePeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) Create(ctx context.Context, iface *domain.Interface) (*domain.Interface, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createdInterface, err := s.interfaces.CreateInterface(ctx, iface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdInterface, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) Update(ctx context.Context, id domain.InterfaceIdentifier, iface *domain.Interface) (
|
||||||
|
*domain.Interface,
|
||||||
|
[]domain.Peer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if iface.Identifier != id {
|
||||||
|
return nil, nil, fmt.Errorf("interface id mismatch: %s != %s: %w",
|
||||||
|
iface.Identifier, id, domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedInterface, updatedPeers, err := s.interfaces.UpdateInterface(ctx, iface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedInterface, updatedPeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) Delete(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.interfaces.DeleteInterface(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
131
internal/app/api/v1/backend/metrics_service.go
Normal file
131
internal/app/api/v1/backend/metrics_service.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MetricsServiceDatabaseRepo interface {
|
||||||
|
GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error)
|
||||||
|
GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
|
*domain.InterfaceStatus,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsServiceUserManagerRepo interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsServicePeerManagerRepo interface {
|
||||||
|
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
db MetricsServiceDatabaseRepo
|
||||||
|
users MetricsServiceUserManagerRepo
|
||||||
|
peers MetricsServicePeerManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMetricsService(
|
||||||
|
cfg *config.Config,
|
||||||
|
db MetricsServiceDatabaseRepo,
|
||||||
|
users MetricsServiceUserManagerRepo,
|
||||||
|
peers MetricsServicePeerManagerRepo,
|
||||||
|
) *MetricsService {
|
||||||
|
return &MetricsService{
|
||||||
|
cfg: cfg,
|
||||||
|
db: db,
|
||||||
|
users: users,
|
||||||
|
peers: peers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MetricsService) GetForInterface(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
|
*domain.InterfaceStatus,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if !m.cfg.Statistics.CollectInterfaceData {
|
||||||
|
return nil, fmt.Errorf("interface statistics collection is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate admin rights
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceStats, err := m.db.GetInterfaceStats(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch stats for interface %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaceStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MetricsService) GetForUser(ctx context.Context, id domain.UserIdentifier) (
|
||||||
|
*domain.User,
|
||||||
|
[]domain.PeerStatus,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if !m.cfg.Statistics.CollectPeerData {
|
||||||
|
return nil, nil, fmt.Errorf("statistics collection is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := m.users.GetUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, err := m.db.GetUserPeers(ctx, user.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to fetch peers for user %s: %w", user.Identifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peerIds := make([]domain.PeerIdentifier, len(peers))
|
||||||
|
for i, peer := range peers {
|
||||||
|
peerIds[i] = peer.Identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
peerStats, err := m.db.GetPeersStats(ctx, peerIds...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to fetch peer stats for user %s: %w", user.Identifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, peerStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MetricsService) GetForPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.PeerStatus, error) {
|
||||||
|
if !m.cfg.Statistics.CollectPeerData {
|
||||||
|
return nil, fmt.Errorf("peer statistics collection is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := m.peers.GetPeer(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerStats, err := m.db.GetPeersStats(ctx, peer.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch stats for peer %s: %w", peer.Identifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(peerStats) == 0 {
|
||||||
|
return nil, fmt.Errorf("no stats found for peer %s: %w", peer.Identifier, domain.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &peerStats[0], nil
|
||||||
|
}
|
143
internal/app/api/v1/backend/peer_service.go
Normal file
143
internal/app/api/v1/backend/peer_service.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PeerServicePeerManagerRepo interface {
|
||||||
|
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
|
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
|
||||||
|
UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
|
||||||
|
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerServiceUserManagerRepo interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
peers PeerServicePeerManagerRepo
|
||||||
|
users PeerServiceUserManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeerService(
|
||||||
|
cfg *config.Config,
|
||||||
|
peers PeerServicePeerManagerRepo,
|
||||||
|
users PeerServiceUserManagerRepo,
|
||||||
|
) *PeerService {
|
||||||
|
return &PeerService{
|
||||||
|
cfg: cfg,
|
||||||
|
peers: peers,
|
||||||
|
users: users,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) GetForInterface(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, interfacePeers, err := s.peers.GetInterfaceAndPeers(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfacePeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) GetForUser(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||||
|
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.users.GetUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userPeers, err := s.peers.GetUserPeers(ctx, user.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userPeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) GetById(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
|
||||||
|
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||||
|
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := s.peers.GetPeer(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has access rights to the requested peer.
|
||||||
|
// If the peer is not linked to any user, access is granted only for admins.
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return peer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) Create(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
|
||||||
|
return nil, fmt.Errorf("peer id mismatch: %s != %s: %w",
|
||||||
|
peer.Identifier, peer.Interface.PublicKey, domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdPeer, err := s.peers.CreatePeer(ctx, peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdPeer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) Update(ctx context.Context, _ domain.PeerIdentifier, peer *domain.Peer) (
|
||||||
|
*domain.Peer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedPeer, err := s.peers.UpdatePeer(ctx, peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedPeer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) Delete(ctx context.Context, id domain.PeerIdentifier) error {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.peers.DeletePeer(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
174
internal/app/api/v1/backend/provisioning_service.go
Normal file
174
internal/app/api/v1/backend/provisioning_service.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProvisioningServiceUserManagerRepo interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningServicePeerManagerRepo interface {
|
||||||
|
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
GetUserPeers(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
|
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
|
||||||
|
CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningServiceConfigFileManagerRepo interface {
|
||||||
|
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
||||||
|
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
users ProvisioningServiceUserManagerRepo
|
||||||
|
peers ProvisioningServicePeerManagerRepo
|
||||||
|
configFiles ProvisioningServiceConfigFileManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvisioningService(
|
||||||
|
cfg *config.Config,
|
||||||
|
users ProvisioningServiceUserManagerRepo,
|
||||||
|
peers ProvisioningServicePeerManagerRepo,
|
||||||
|
configFiles ProvisioningServiceConfigFileManagerRepo,
|
||||||
|
) *ProvisioningService {
|
||||||
|
return &ProvisioningService{
|
||||||
|
cfg: cfg,
|
||||||
|
|
||||||
|
users: users,
|
||||||
|
peers: peers,
|
||||||
|
configFiles: configFiles,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) GetUserAndPeers(
|
||||||
|
ctx context.Context,
|
||||||
|
userId domain.UserIdentifier,
|
||||||
|
email string,
|
||||||
|
) (*domain.User, []domain.Peer, error) {
|
||||||
|
// first fetch user
|
||||||
|
var user *domain.User
|
||||||
|
switch {
|
||||||
|
case userId != "":
|
||||||
|
u, err := p.users.GetUser(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
user = u
|
||||||
|
case email != "":
|
||||||
|
u, err := p.users.GetUserByEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
user = u
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("either UserId or Email must be set: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, user.Identifier); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, err := p.peers.GetUserPeers(ctx, user.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, peers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) GetPeerConfig(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error) {
|
||||||
|
peer, err := p.peers.GetPeer(ctx, peerId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgData, err := io.ReadAll(peerCfgReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerCfgData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) GetPeerQrPng(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error) {
|
||||||
|
peer, err := p.peers.GetPeer(ctx, peerId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgQrData, err := io.ReadAll(peerCfgQrReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerCfgQrData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) NewPeer(ctx context.Context, req models.ProvisioningRequest) (*domain.Peer, error) {
|
||||||
|
if req.UserIdentifier == "" {
|
||||||
|
req.UserIdentifier = string(domain.GetUserInfo(ctx).Id) // use authenticated user id if not set
|
||||||
|
}
|
||||||
|
|
||||||
|
// check permissions
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, domain.UserIdentifier(req.UserIdentifier)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !p.cfg.Core.SelfProvisioningAllowed {
|
||||||
|
// only admins can create new peers if self-provisioning is disabled
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare new peer
|
||||||
|
peer, err := p.peers.PreparePeer(ctx, domain.InterfaceIdentifier(req.InterfaceIdentifier))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to prepare new peer: %w", err)
|
||||||
|
}
|
||||||
|
peer.UserIdentifier = domain.UserIdentifier(req.UserIdentifier) // overwrite context user id with the one from the request
|
||||||
|
if req.PublicKey != "" {
|
||||||
|
peer.Identifier = domain.PeerIdentifier(req.PublicKey)
|
||||||
|
peer.Interface.PublicKey = req.PublicKey
|
||||||
|
peer.Interface.PrivateKey = "" // clear private key if public key is set, WireGuard Portal does not know the private key in that case
|
||||||
|
}
|
||||||
|
if req.PresharedKey != "" {
|
||||||
|
peer.PresharedKey = domain.PreSharedKey(req.PresharedKey)
|
||||||
|
}
|
||||||
|
peer.GenerateDisplayName("API")
|
||||||
|
|
||||||
|
// save new peer
|
||||||
|
peer, err = p.peers.CreatePeer(ctx, peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new peer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return peer, nil
|
||||||
|
}
|
107
internal/app/api/v1/backend/user_service.go
Normal file
107
internal/app/api/v1/backend/user_service.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserManagerRepo interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||||
|
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
|
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
|
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
users UserManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserService(cfg *config.Config, users UserManagerRepo) *UserService {
|
||||||
|
return &UserService{
|
||||||
|
cfg: cfg,
|
||||||
|
users: users,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) GetAll(ctx context.Context) ([]domain.User, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allUsers, err := s.users.GetAllUsers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return allUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) GetById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||||
|
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.users.GetUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) Create(ctx context.Context, user *domain.User) (*domain.User, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createdUser, err := s.users.CreateUser(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) Update(ctx context.Context, id domain.UserIdentifier, user *domain.User) (
|
||||||
|
*domain.User,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != user.Identifier {
|
||||||
|
return nil, fmt.Errorf("user id mismatch: %s != %s: %w", id, user.Identifier, domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedUser, err := s.users.UpdateUser(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) Delete(ctx context.Context, id domain.UserIdentifier) error {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.users.DeleteUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
81
internal/app/api/v1/handlers/base.go
Normal file
81
internal/app/api/v1/handlers/base.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/core"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
GetName() string
|
||||||
|
RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To compile the API documentation use the
|
||||||
|
// api_build_tool
|
||||||
|
// command that can be found in the $PROJECT_ROOT/cmd/api_build_tool directory.
|
||||||
|
|
||||||
|
// @title WireGuard Portal Public API
|
||||||
|
// @version 1.0
|
||||||
|
// @description The WireGuard Portal REST API enables efficient management of WireGuard VPN configurations through a set of JSON-based endpoints.
|
||||||
|
// @description It supports creating and editing peers, interfaces, and user profiles, while also providing role-based access control and auditing.
|
||||||
|
// @description This API allows seamless integration with external tools or scripts for automated network configuration and administration.
|
||||||
|
|
||||||
|
// @license.name MIT
|
||||||
|
// @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
// @contact.name WireGuard Portal Project
|
||||||
|
// @contact.url https://github.com/h44z/wg-portal
|
||||||
|
|
||||||
|
// @securityDefinitions.basic BasicAuth
|
||||||
|
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @query.collection.format multi
|
||||||
|
|
||||||
|
func NewRestApi(userSource UserSource, handlers ...Handler) core.ApiEndpointSetupFunc {
|
||||||
|
authenticator := &authenticationHandler{
|
||||||
|
userSource: userSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() (core.ApiVersion, core.GroupSetupFn) {
|
||||||
|
return "v1", func(group *gin.RouterGroup) {
|
||||||
|
group.Use(cors.Default())
|
||||||
|
|
||||||
|
// Handler functions
|
||||||
|
for _, h := range handlers {
|
||||||
|
h.RegisterRoutes(group, authenticator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseServiceError(err error) (int, models.Error) {
|
||||||
|
if err == nil {
|
||||||
|
return 500, models.Error{
|
||||||
|
Code: 500,
|
||||||
|
Message: "unknown server error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code := http.StatusInternalServerError
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, domain.ErrNotFound):
|
||||||
|
code = http.StatusNotFound
|
||||||
|
case errors.Is(err, domain.ErrNoPermission):
|
||||||
|
code = http.StatusForbidden
|
||||||
|
case errors.Is(err, domain.ErrDuplicateEntry):
|
||||||
|
code = http.StatusConflict
|
||||||
|
case errors.Is(err, domain.ErrInvalidData):
|
||||||
|
code = http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, models.Error{
|
||||||
|
Code: code,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
220
internal/app/api/v1/handlers/endpoint_interface.go
Normal file
220
internal/app/api/v1/handlers/endpoint_interface.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InterfaceEndpointInterfaceService interface {
|
||||||
|
GetAll(context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||||
|
GetById(context.Context, domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
Create(context.Context, *domain.Interface) (*domain.Interface, error)
|
||||||
|
Update(context.Context, domain.InterfaceIdentifier, *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||||
|
Delete(context.Context, domain.InterfaceIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceEndpoint struct {
|
||||||
|
interfaces InterfaceEndpointInterfaceService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterfaceEndpoint(interfaceService InterfaceEndpointInterfaceService) *InterfaceEndpoint {
|
||||||
|
return &InterfaceEndpoint{
|
||||||
|
interfaces: interfaceService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e InterfaceEndpoint) GetName() string {
|
||||||
|
return "InterfaceEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e InterfaceEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/interface", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
||||||
|
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleByIdGet())
|
||||||
|
|
||||||
|
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
|
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||||
|
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID interface_handleAllGet
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Get all interface records.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.Interface
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/all [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleAllGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
allInterfaces, allPeersPerInterface, err := e.interfaces.GetAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterfaces(allInterfaces, allPeersPerInterface))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleByIdGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleByIdGet
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Get a specific interface record by its identifier.
|
||||||
|
// @Param id path string true "The interface identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Interface
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/by-id/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iface, interfacePeers, err := e.interfaces.GetById(ctx, domain.InterfaceIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterface(iface, interfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreatePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleCreatePost
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Create a new interface record.
|
||||||
|
// @Param request body models.Interface true "The interface data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Interface
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 409 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/new [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var iface models.Interface
|
||||||
|
err := c.BindJSON(&iface)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newInterface, err := e.interfaces.Create(ctx, models.NewDomainInterface(&iface))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterface(newInterface, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdatePut returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleUpdatePut
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Update an interface record.
|
||||||
|
// @Param id path string true "The interface identifier."
|
||||||
|
// @Param request body models.Interface true "The interface data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Interface
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/by-id/{id} [put]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var iface models.Interface
|
||||||
|
err := c.BindJSON(&iface)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedInterface, updatedInterfacePeers, err := e.interfaces.Update(
|
||||||
|
ctx,
|
||||||
|
domain.InterfaceIdentifier(id),
|
||||||
|
models.NewDomainInterface(&iface),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterface(updatedInterface, updatedInterfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelete returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleDelete
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Delete the interface record.
|
||||||
|
// @Param id path string true "The interface identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204 "No content if deletion was successful."
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/by-id/{id} [delete]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleDelete() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.interfaces.Delete(ctx, domain.InterfaceIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
140
internal/app/api/v1/handlers/endpoint_metrics.go
Normal file
140
internal/app/api/v1/handlers/endpoint_metrics.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MetricsEndpointStatisticsService interface {
|
||||||
|
GetForInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.InterfaceStatus, error)
|
||||||
|
GetForUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, []domain.PeerStatus, error)
|
||||||
|
GetForPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.PeerStatus, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsEndpoint struct {
|
||||||
|
metrics MetricsEndpointStatisticsService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMetricsEndpoint(metrics MetricsEndpointStatisticsService) *MetricsEndpoint {
|
||||||
|
return &MetricsEndpoint{
|
||||||
|
metrics: metrics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MetricsEndpoint) GetName() string {
|
||||||
|
return "MetricsEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MetricsEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/metrics", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/by-interface/:id", authenticator.LoggedIn(ScopeAdmin), e.handleMetricsForInterfaceGet())
|
||||||
|
apiGroup.GET("/by-user/:id", authenticator.LoggedIn(), e.handleMetricsForUserGet())
|
||||||
|
apiGroup.GET("/by-peer/:id", authenticator.LoggedIn(), e.handleMetricsForPeerGet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMetricsForInterfaceGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID metrics_handleMetricsForInterfaceGet
|
||||||
|
// @Tags Metrics
|
||||||
|
// @Summary Get all metrics for a WireGuard Portal interface.
|
||||||
|
// @Param id path string true "The WireGuard interface identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.InterfaceMetrics
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /metrics/by-interface/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e MetricsEndpoint) handleMetricsForInterfaceGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceMetrics, err := e.metrics.GetForInterface(ctx, domain.InterfaceIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterfaceMetrics(interfaceMetrics))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMetricsForUserGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID metrics_handleMetricsForUserGet
|
||||||
|
// @Tags Metrics
|
||||||
|
// @Summary Get all metrics for a WireGuard Portal user.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.UserMetrics
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /metrics/by-user/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e MetricsEndpoint) handleMetricsForUserGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, userMetrics, err := e.metrics.GetForUser(ctx, domain.UserIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUserMetrics(user, userMetrics))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMetricsForPeerGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID metrics_handleMetricsForPeerGet
|
||||||
|
// @Tags Metrics
|
||||||
|
// @Summary Get all metrics for a WireGuard Portal peer.
|
||||||
|
// @Param id path string true "The peer identifier (public key)."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.PeerMetrics
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /metrics/by-peer/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e MetricsEndpoint) handleMetricsForPeerGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerMetrics, err := e.metrics.GetForPeer(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeerMetrics(peerMetrics))
|
||||||
|
}
|
||||||
|
}
|
261
internal/app/api/v1/handlers/endpoint_peer.go
Normal file
261
internal/app/api/v1/handlers/endpoint_peer.go
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PeerService interface {
|
||||||
|
GetForInterface(context.Context, domain.InterfaceIdentifier) ([]domain.Peer, error)
|
||||||
|
GetForUser(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
|
GetById(context.Context, domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
Create(context.Context, *domain.Peer) (*domain.Peer, error)
|
||||||
|
Update(context.Context, domain.PeerIdentifier, *domain.Peer) (*domain.Peer, error)
|
||||||
|
Delete(context.Context, domain.PeerIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerEndpoint struct {
|
||||||
|
peers PeerService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeerEndpoint(peerService PeerService) *PeerEndpoint {
|
||||||
|
return &PeerEndpoint{
|
||||||
|
peers: peerService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PeerEndpoint) GetName() string {
|
||||||
|
return "PeerEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PeerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/peer", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/by-interface/:id", authenticator.LoggedIn(ScopeAdmin), e.handleAllForInterfaceGet())
|
||||||
|
apiGroup.GET("/by-user/:id", authenticator.LoggedIn(), e.handleAllForUserGet())
|
||||||
|
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
|
||||||
|
|
||||||
|
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
|
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||||
|
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllForInterfaceGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleAllForInterfaceGet
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Get all peer records for a given WireGuard interface.
|
||||||
|
// @Param id path string true "The WireGuard interface identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.Peer
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-interface/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleAllForInterfaceGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interfacePeers, err := e.peers.GetForInterface(ctx, domain.InterfaceIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllForUserGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleAllForUserGet
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Get all peer records for a given user.
|
||||||
|
// @Description Normal users can only access their own records. Admins can access all records.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.Peer
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-user/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleAllForUserGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interfacePeers, err := e.peers.GetForUser(ctx, domain.UserIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleByIdGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleByIdGet
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Get a specific peer record by its identifier (public key).
|
||||||
|
// @Description Normal users can only access their own records. Admins can access all records.
|
||||||
|
// @Param id path string true "The peer identifier (public key)."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-id/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := e.peers.GetById(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreatePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleCreatePost
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Create a new peer record.
|
||||||
|
// @Description Only admins can create new records.
|
||||||
|
// @Param request body models.Peer true "The peer data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 409 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/new [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var peer models.Peer
|
||||||
|
err := c.BindJSON(&peer)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPeer, err := e.peers.Create(ctx, models.NewDomainPeer(&peer))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(newPeer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdatePut returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleUpdatePut
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Update a peer record.
|
||||||
|
// @Description Only admins can update existing records.
|
||||||
|
// @Param id path string true "The peer identifier."
|
||||||
|
// @Param request body models.Peer true "The peer data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-id/{id} [put]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var peer models.Peer
|
||||||
|
err := c.BindJSON(&peer)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedPeer, err := e.peers.Update(ctx, domain.PeerIdentifier(id), models.NewDomainPeer(&peer))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(updatedPeer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelete returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleDelete
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Delete the peer record.
|
||||||
|
// @Param id path string true "The peer identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204 "No content if deletion was successful."
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-id/{id} [delete]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleDelete() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.peers.Delete(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
195
internal/app/api/v1/handlers/endpoint_provisioning.go
Normal file
195
internal/app/api/v1/handlers/endpoint_provisioning.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProvisioningEndpointProvisioningService interface {
|
||||||
|
GetUserAndPeers(ctx context.Context, userId domain.UserIdentifier, email string) (
|
||||||
|
*domain.User,
|
||||||
|
[]domain.Peer,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
GetPeerConfig(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
|
||||||
|
GetPeerQrPng(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
|
||||||
|
NewPeer(ctx context.Context, req models.ProvisioningRequest) (*domain.Peer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningEndpoint struct {
|
||||||
|
provisioning ProvisioningEndpointProvisioningService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvisioningEndpoint(provisioning ProvisioningEndpointProvisioningService) *ProvisioningEndpoint {
|
||||||
|
return &ProvisioningEndpoint{
|
||||||
|
provisioning: provisioning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ProvisioningEndpoint) GetName() string {
|
||||||
|
return "ProvisioningEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ProvisioningEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/provisioning", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/data/user-info", authenticator.LoggedIn(), e.handleUserInfoGet())
|
||||||
|
apiGroup.GET("/data/peer-config", authenticator.LoggedIn(), e.handlePeerConfigGet())
|
||||||
|
apiGroup.GET("/data/peer-qr", authenticator.LoggedIn(), e.handlePeerQrGet())
|
||||||
|
|
||||||
|
apiGroup.POST("/new-peer", authenticator.LoggedIn(), e.handleNewPeerPost())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUserInfoGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handleUserInfoGet
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Get information about all peer records for a given user.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param UserId query string false "The user identifier that should be queried. If not set, the authenticated user is used."
|
||||||
|
// @Param Email query string false "The email address that should be queried. If UserId is set, this is ignored."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.UserInformation
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/data/user-info [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handleUserInfoGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := strings.TrimSpace(c.Query("UserId"))
|
||||||
|
email := strings.TrimSpace(c.Query("Email"))
|
||||||
|
|
||||||
|
if id == "" && email == "" {
|
||||||
|
id = string(domain.GetUserInfo(ctx).Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, peers, err := e.provisioning.GetUserAndPeers(ctx, domain.UserIdentifier(id), email)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUserInformation(user, peers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePeerConfigGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handlePeerConfigGet
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Get the peer configuration in wg-quick format.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
|
||||||
|
// @Produce plain
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {string} string "The WireGuard configuration file"
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/data/peer-config [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handlePeerConfigGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := strings.TrimSpace(c.Query("PeerId"))
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConfig, err := e.provisioning.GetPeerConfig(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, "text/plain", peerConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePeerQrGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handlePeerQrGet
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Get the peer configuration as QR code.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
|
||||||
|
// @Produce png
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {file} binary "The WireGuard configuration QR code"
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/data/peer-qr [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handlePeerQrGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := strings.TrimSpace(c.Query("PeerId"))
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConfigQrCode, err := e.provisioning.GetPeerQrPng(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, "image/png", peerConfigQrCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNewPeerPost returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handleNewPeerPost
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Create a new peer for the given interface and user.
|
||||||
|
// @Description Normal users can only create new peers if self provisioning is allowed. Admins can always add new peers.
|
||||||
|
// @Param request body models.ProvisioningRequest true "Provisioning request model."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/new-peer [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handleNewPeerPost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var req models.ProvisioningRequest
|
||||||
|
err := c.BindJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := e.provisioning.NewPeer(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(peer))
|
||||||
|
}
|
||||||
|
}
|
218
internal/app/api/v1/handlers/endpoint_user.go
Normal file
218
internal/app/api/v1/handlers/endpoint_user.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserService interface {
|
||||||
|
GetAll(ctx context.Context) ([]domain.User, error)
|
||||||
|
GetById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
Create(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
|
Update(ctx context.Context, id domain.UserIdentifier, user *domain.User) (*domain.User, error)
|
||||||
|
Delete(ctx context.Context, id domain.UserIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserEndpoint struct {
|
||||||
|
users UserService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserEndpoint(userService UserService) *UserEndpoint {
|
||||||
|
return &UserEndpoint{
|
||||||
|
users: userService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UserEndpoint) GetName() string {
|
||||||
|
return "UserEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UserEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/user", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
||||||
|
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
|
||||||
|
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
|
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||||
|
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleAllGet
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Get all user records.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.User
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/all [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleAllGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
users, err := e.users.GetAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUsers(users))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleByIdGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleByIdGet
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Get a specific user record by its internal identifier.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/by-id/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := e.users.GetById(ctx, domain.UserIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUser(user, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreatePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleCreatePost
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Create a new user record.
|
||||||
|
// @Description Only admins can create new records.
|
||||||
|
// @Param request body models.User true "The user data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 409 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/new [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err := c.BindJSON(&user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newUser, err := e.users.Create(ctx, models.NewDomainUser(&user))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUser(newUser, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdatePut returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleUpdatePut
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Update a user record.
|
||||||
|
// @Description Only admins can update existing records.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Param request body models.User true "The user data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/by-id/{id} [put]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err := c.BindJSON(&user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser, err := e.users.Update(ctx, domain.UserIdentifier(id), models.NewDomainUser(&user))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUser(updateUser, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelete returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleDelete
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Delete the user record.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204 "No content if deletion was successful."
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/by-id/{id} [delete]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleDelete() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.users.Delete(ctx, domain.UserIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
92
internal/app/api/v1/handlers/middleware_authentication.go
Normal file
92
internal/app/api/v1/handlers/middleware_authentication.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scope string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScopeAdmin Scope = "ADMIN" // Admin scope contains all other scopes
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserSource interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type authenticationHandler struct {
|
||||||
|
userSource UserSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggedIn checks if a user is logged in. If scopes are given, they are validated as well.
|
||||||
|
func (h authenticationHandler) LoggedIn(scopes ...Scope) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
username, password, ok := c.Request.BasicAuth()
|
||||||
|
if !ok || username == "" || password == "" {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "missing credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user exists in DB
|
||||||
|
|
||||||
|
ctx := domain.SetUserInfo(c.Request.Context(), domain.SystemAdminContextUserInfo())
|
||||||
|
user, err := h.userSource.GetUser(ctx, domain.UserIdentifier(username))
|
||||||
|
if err != nil {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate API token
|
||||||
|
if err := user.CheckApiToken(password); err != nil {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !UserHasScopes(user, scopes...) {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusForbidden, model.Error{Code: http.StatusForbidden, Message: "not enough permissions"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set(domain.CtxUserInfo, &domain.ContextUserInfo{
|
||||||
|
Id: user.Identifier,
|
||||||
|
IsAdmin: user.IsAdmin,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Continue down the chain to Handler etc
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserHasScopes(user *domain.User, scopes ...Scope) bool {
|
||||||
|
// No scopes give, so the check should succeed
|
||||||
|
if len(scopes) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user has admin scope
|
||||||
|
if user.IsAdmin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if admin scope is required
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if scope == ScopeAdmin {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
46
internal/app/api/v1/models/model_options.go
Normal file
46
internal/app/api/v1/models/model_options.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigOption[T any] struct {
|
||||||
|
Value T `json:"Value"`
|
||||||
|
Overridable bool `json:"Overridable,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigOption[T any](value T, overridable bool) ConfigOption[T] {
|
||||||
|
return ConfigOption[T]{
|
||||||
|
Value: value,
|
||||||
|
Overridable: overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigOptionFromDomain[T any](opt domain.ConfigOption[T]) ConfigOption[T] {
|
||||||
|
return ConfigOption[T]{
|
||||||
|
Value: opt.Value,
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigOptionToDomain[T any](opt ConfigOption[T]) domain.ConfigOption[T] {
|
||||||
|
return domain.ConfigOption[T]{
|
||||||
|
Value: opt.Value,
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSliceConfigOptionFromDomain(opt domain.ConfigOption[string]) ConfigOption[[]string] {
|
||||||
|
return ConfigOption[[]string]{
|
||||||
|
Value: internal.SliceString(opt.Value),
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSliceConfigOptionToDomain(opt ConfigOption[[]string]) domain.ConfigOption[string] {
|
||||||
|
return domain.ConfigOption[string]{
|
||||||
|
Value: internal.SliceToString(opt.Value),
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
8
internal/app/api/v1/models/models.go
Normal file
8
internal/app/api/v1/models/models.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Error represents an error response.
|
||||||
|
type Error struct {
|
||||||
|
Code int `json:"Code"` // HTTP status code.
|
||||||
|
Message string `json:"Message"` // Error message.
|
||||||
|
Details string `json:"Details,omitempty"` // Additional error details.
|
||||||
|
}
|
201
internal/app/api/v1/models/models_interface.go
Normal file
201
internal/app/api/v1/models/models_interface.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface represents a WireGuard interface.
|
||||||
|
type Interface struct {
|
||||||
|
// Identifier is the unique identifier of the interface. It is always equal to the device name of the interface.
|
||||||
|
Identifier string `json:"Identifier" example:"wg0" binding:"required"`
|
||||||
|
// DisplayName is a nice display name / description for the interface.
|
||||||
|
DisplayName string `json:"DisplayName" binding:"omitempty,max=64" example:"My Interface"`
|
||||||
|
// Mode is the interface type, either 'server', 'client' or 'any'. The mode specifies how WireGuard Portal handles peers for this interface.
|
||||||
|
Mode string `json:"Mode" example:"server" binding:"required,oneof=server client any"`
|
||||||
|
// PrivateKey is the private key of the interface.
|
||||||
|
PrivateKey string `json:"PrivateKey" example:"gI6EdUSYvn8ugXOt8QQD6Yc+JyiZxIhp3GInSWRfWGE=" binding:"required,len=44"`
|
||||||
|
// PublicKey is the public key of the server interface. The public key is used by peers to connect to the server.
|
||||||
|
PublicKey string `json:"PublicKey" example:"HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=" binding:"required,len=44"`
|
||||||
|
// Disabled is a flag that specifies if the interface is enabled (up) or not (down). Disabled interfaces are not able to accept connections.
|
||||||
|
Disabled bool `json:"Disabled" example:"false"`
|
||||||
|
// DisabledReason is the reason why the interface has been disabled.
|
||||||
|
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:"This is a reason why the interface has been disabled."`
|
||||||
|
// SaveConfig is a flag that specifies if the configuration should be saved to the configuration file (wgX.conf in wg-quick format).
|
||||||
|
SaveConfig bool `json:"SaveConfig" example:"false"`
|
||||||
|
|
||||||
|
// ListenPort is the listening port, for example: 51820. The listening port is only required for server interfaces.
|
||||||
|
ListenPort int `json:"ListenPort" binding:"omitempty,min=1,max=65535" example:"51820"`
|
||||||
|
// Addresses is a list of IP addresses (in CIDR format) that are assigned to the interface.
|
||||||
|
Addresses []string `json:"Addresses" binding:"omitempty,dive,cidr" example:"10.11.12.1/24"`
|
||||||
|
// Dns is a list of DNS servers that should be set if the interface is up.
|
||||||
|
Dns []string `json:"Dns" binding:"omitempty,dive,ip" example:"1.1.1.1"`
|
||||||
|
// DnsSearch is the dns search option string that should be set if the interface is up, will be appended to Dns servers.
|
||||||
|
DnsSearch []string `json:"DnsSearch" binding:"omitempty,dive,fqdn" example:"wg.local"`
|
||||||
|
// Mtu is the device MTU of the interface.
|
||||||
|
Mtu int `json:"Mtu" binding:"omitempty,min=1,max=9000" example:"1420"`
|
||||||
|
// FirewallMark is an optional firewall mark which is used to handle interface traffic.
|
||||||
|
FirewallMark uint32 `json:"FirewallMark"`
|
||||||
|
// RoutingTable is an optional routing table which is used to route interface traffic.
|
||||||
|
RoutingTable string `json:"RoutingTable"`
|
||||||
|
|
||||||
|
// PreUp is an optional action that is executed before the device is up.
|
||||||
|
PreUp string `json:"PreUp" example:"echo 'Interface is up'"`
|
||||||
|
// PostUp is an optional action that is executed after the device is up.
|
||||||
|
PostUp string `json:"PostUp" example:"iptables -A FORWARD -i %i -j ACCEPT"`
|
||||||
|
// PreDown is an optional action that is executed before the device is down.
|
||||||
|
PreDown string `json:"PreDown" example:"iptables -D FORWARD -i %i -j ACCEPT"`
|
||||||
|
// PostDown is an optional action that is executed after the device is down.
|
||||||
|
PostDown string `json:"PostDown" example:"echo 'Interface is down'"`
|
||||||
|
|
||||||
|
// PeerDefNetwork specifies the default subnets from which new peers will get their IP addresses. The subnet is specified in CIDR format.
|
||||||
|
PeerDefNetwork []string `json:"PeerDefNetwork" example:"10.11.12.0/24"`
|
||||||
|
// PeerDefDns specifies the default dns servers for a new peer.
|
||||||
|
PeerDefDns []string `json:"PeerDefDns" example:"8.8.8.8"`
|
||||||
|
// PeerDefDnsSearch specifies the default dns search options for a new peer.
|
||||||
|
PeerDefDnsSearch []string `json:"PeerDefDnsSearch" example:"wg.local"`
|
||||||
|
// PeerDefEndpoint specifies the default endpoint for a new peer.
|
||||||
|
PeerDefEndpoint string `json:"PeerDefEndpoint" example:"wg.example.com:51820"`
|
||||||
|
// PeerDefAllowedIPs specifies the default allowed IP addresses for a new peer.
|
||||||
|
PeerDefAllowedIPs []string `json:"PeerDefAllowedIPs" example:"10.11.12.0/24"`
|
||||||
|
// PeerDefMtu specifies the default device MTU for a new peer.
|
||||||
|
PeerDefMtu int `json:"PeerDefMtu" example:"1420"`
|
||||||
|
// PeerDefPersistentKeepalive specifies the default persistent keep-alive value in seconds for a new peer.
|
||||||
|
PeerDefPersistentKeepalive int `json:"PeerDefPersistentKeepalive" example:"25"`
|
||||||
|
// PeerDefFirewallMark specifies the default firewall mark for a new peer.
|
||||||
|
PeerDefFirewallMark uint32 `json:"PeerDefFirewallMark"`
|
||||||
|
// PeerDefRoutingTable specifies the default routing table for a new peer.
|
||||||
|
PeerDefRoutingTable string `json:"PeerDefRoutingTable"`
|
||||||
|
|
||||||
|
// PeerDefPreUp specifies the default action that is executed before the device is up for a new peer.
|
||||||
|
PeerDefPreUp string `json:"PeerDefPreUp"`
|
||||||
|
// PeerDefPostUp specifies the default action that is executed after the device is up for a new peer.
|
||||||
|
PeerDefPostUp string `json:"PeerDefPostUp"`
|
||||||
|
// PeerDefPreDown specifies the default action that is executed before the device is down for a new peer.
|
||||||
|
PeerDefPreDown string `json:"PeerDefPreDown"`
|
||||||
|
// PeerDefPostDown specifies the default action that is executed after the device is down for a new peer.
|
||||||
|
PeerDefPostDown string `json:"PeerDefPostDown"`
|
||||||
|
|
||||||
|
// Calculated values
|
||||||
|
|
||||||
|
// EnabledPeers is the number of enabled peers for this interface. Only enabled peers are able to connect.
|
||||||
|
EnabledPeers int `json:"EnabledPeers" readonly:"true"`
|
||||||
|
// TotalPeers is the total number of peers for this interface.
|
||||||
|
TotalPeers int `json:"TotalPeers" readonly:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
|
||||||
|
iface := &Interface{
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
Mode: string(src.Type),
|
||||||
|
PrivateKey: src.PrivateKey,
|
||||||
|
PublicKey: src.PublicKey,
|
||||||
|
Disabled: src.IsDisabled(),
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
SaveConfig: src.SaveConfig,
|
||||||
|
ListenPort: src.ListenPort,
|
||||||
|
Addresses: domain.CidrsToStringSlice(src.Addresses),
|
||||||
|
Dns: internal.SliceString(src.DnsStr),
|
||||||
|
DnsSearch: internal.SliceString(src.DnsSearchStr),
|
||||||
|
Mtu: src.Mtu,
|
||||||
|
FirewallMark: src.FirewallMark,
|
||||||
|
RoutingTable: src.RoutingTable,
|
||||||
|
PreUp: src.PreUp,
|
||||||
|
PostUp: src.PostUp,
|
||||||
|
PreDown: src.PreDown,
|
||||||
|
PostDown: src.PostDown,
|
||||||
|
PeerDefNetwork: internal.SliceString(src.PeerDefNetworkStr),
|
||||||
|
PeerDefDns: internal.SliceString(src.PeerDefDnsStr),
|
||||||
|
PeerDefDnsSearch: internal.SliceString(src.PeerDefDnsSearchStr),
|
||||||
|
PeerDefEndpoint: src.PeerDefEndpoint,
|
||||||
|
PeerDefAllowedIPs: internal.SliceString(src.PeerDefAllowedIPsStr),
|
||||||
|
PeerDefMtu: src.PeerDefMtu,
|
||||||
|
PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive,
|
||||||
|
PeerDefFirewallMark: src.PeerDefFirewallMark,
|
||||||
|
PeerDefRoutingTable: src.PeerDefRoutingTable,
|
||||||
|
PeerDefPreUp: src.PeerDefPreUp,
|
||||||
|
PeerDefPostUp: src.PeerDefPostUp,
|
||||||
|
PeerDefPreDown: src.PeerDefPreDown,
|
||||||
|
PeerDefPostDown: src.PeerDefPostDown,
|
||||||
|
|
||||||
|
EnabledPeers: 0,
|
||||||
|
TotalPeers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(peers) > 0 {
|
||||||
|
iface.TotalPeers = len(peers)
|
||||||
|
|
||||||
|
activePeers := 0
|
||||||
|
for _, peer := range peers {
|
||||||
|
if !peer.IsDisabled() {
|
||||||
|
activePeers++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iface.EnabledPeers = activePeers
|
||||||
|
}
|
||||||
|
|
||||||
|
return iface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface {
|
||||||
|
results := make([]Interface, len(src))
|
||||||
|
for i := range src {
|
||||||
|
results[i] = *NewInterface(&src[i], srcPeers[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainInterface(src *Interface) *domain.Interface {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
cidrs, _ := domain.CidrsFromArray(src.Addresses)
|
||||||
|
|
||||||
|
res := &domain.Interface{
|
||||||
|
BaseModel: domain.BaseModel{},
|
||||||
|
Identifier: domain.InterfaceIdentifier(src.Identifier),
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PrivateKey: src.PrivateKey,
|
||||||
|
PublicKey: src.PublicKey,
|
||||||
|
},
|
||||||
|
ListenPort: src.ListenPort,
|
||||||
|
Addresses: cidrs,
|
||||||
|
DnsStr: internal.SliceToString(src.Dns),
|
||||||
|
DnsSearchStr: internal.SliceToString(src.DnsSearch),
|
||||||
|
Mtu: src.Mtu,
|
||||||
|
FirewallMark: src.FirewallMark,
|
||||||
|
RoutingTable: src.RoutingTable,
|
||||||
|
PreUp: src.PreUp,
|
||||||
|
PostUp: src.PostUp,
|
||||||
|
PreDown: src.PreDown,
|
||||||
|
PostDown: src.PostDown,
|
||||||
|
SaveConfig: src.SaveConfig,
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
Type: domain.InterfaceType(src.Mode),
|
||||||
|
DriverType: "", // currently unused
|
||||||
|
Disabled: nil, // set below
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
PeerDefNetworkStr: internal.SliceToString(src.PeerDefNetwork),
|
||||||
|
PeerDefDnsStr: internal.SliceToString(src.PeerDefDns),
|
||||||
|
PeerDefDnsSearchStr: internal.SliceToString(src.PeerDefDnsSearch),
|
||||||
|
PeerDefEndpoint: src.PeerDefEndpoint,
|
||||||
|
PeerDefAllowedIPsStr: internal.SliceToString(src.PeerDefAllowedIPs),
|
||||||
|
PeerDefMtu: src.PeerDefMtu,
|
||||||
|
PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive,
|
||||||
|
PeerDefFirewallMark: src.PeerDefFirewallMark,
|
||||||
|
PeerDefRoutingTable: src.PeerDefRoutingTable,
|
||||||
|
PeerDefPreUp: src.PeerDefPreUp,
|
||||||
|
PeerDefPostUp: src.PeerDefPostUp,
|
||||||
|
PeerDefPreDown: src.PeerDefPreDown,
|
||||||
|
PeerDefPostDown: src.PeerDefPostDown,
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Disabled {
|
||||||
|
res.Disabled = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
105
internal/app/api/v1/models/models_metrics.go
Normal file
105
internal/app/api/v1/models/models_metrics.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PeerMetrics represents the metrics of a WireGuard peer.
|
||||||
|
type PeerMetrics struct {
|
||||||
|
// The unique identifier of the peer.
|
||||||
|
PeerIdentifier string `json:"PeerIdentifier" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="`
|
||||||
|
|
||||||
|
// If this field is set, the peer is pingable.
|
||||||
|
IsPingable bool `json:"IsPingable" example:"true"`
|
||||||
|
// The last time the peer responded to a ICMP ping request.
|
||||||
|
LastPing *time.Time `json:"LastPing" example:"2021-01-01T12:00:00Z"`
|
||||||
|
|
||||||
|
// The number of bytes received by the peer.
|
||||||
|
BytesReceived uint64 `json:"BytesReceived" example:"123456789"`
|
||||||
|
// The number of bytes transmitted by the peer.
|
||||||
|
BytesTransmitted uint64 `json:"BytesTransmitted" example:"123456789"`
|
||||||
|
|
||||||
|
// The last time the peer initiated a handshake.
|
||||||
|
LastHandshake *time.Time `json:"LastHandshake" example:"2021-01-01T12:00:00Z"`
|
||||||
|
// The current endpoint address of the peer.
|
||||||
|
Endpoint string `json:"Endpoint" example:"12.34.56.78"`
|
||||||
|
// The last time the peer initiated a session.
|
||||||
|
LastSessionStart *time.Time `json:"LastSessionStart" example:"2021-01-01T12:00:00Z"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeerMetrics(src *domain.PeerStatus) *PeerMetrics {
|
||||||
|
return &PeerMetrics{
|
||||||
|
PeerIdentifier: string(src.PeerId),
|
||||||
|
IsPingable: src.IsPingable,
|
||||||
|
LastPing: src.LastPing,
|
||||||
|
BytesReceived: src.BytesReceived,
|
||||||
|
BytesTransmitted: src.BytesTransmitted,
|
||||||
|
LastHandshake: src.LastHandshake,
|
||||||
|
Endpoint: src.Endpoint,
|
||||||
|
LastSessionStart: src.LastSessionStart,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceMetrics represents the metrics of a WireGuard interface.
|
||||||
|
type InterfaceMetrics struct {
|
||||||
|
// The unique identifier of the interface.
|
||||||
|
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0"`
|
||||||
|
|
||||||
|
// The number of bytes received by the interface.
|
||||||
|
BytesReceived uint64 `json:"BytesReceived" example:"123456789"`
|
||||||
|
// The number of bytes transmitted by the interface.
|
||||||
|
BytesTransmitted uint64 `json:"BytesTransmitted" example:"123456789"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterfaceMetrics(src *domain.InterfaceStatus) *InterfaceMetrics {
|
||||||
|
return &InterfaceMetrics{
|
||||||
|
InterfaceIdentifier: string(src.InterfaceId),
|
||||||
|
BytesReceived: src.BytesReceived,
|
||||||
|
BytesTransmitted: src.BytesTransmitted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserMetrics represents the metrics of a WireGuard user.
|
||||||
|
type UserMetrics struct {
|
||||||
|
// The unique identifier of the user.
|
||||||
|
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||||
|
|
||||||
|
// PeerCount represents the number of peers linked to the user.
|
||||||
|
PeerCount int `json:"PeerCount" example:"2"`
|
||||||
|
|
||||||
|
// The total number of bytes received by the user. This is the sum of all bytes received by the peers linked to the user.
|
||||||
|
BytesReceived uint64 `json:"BytesReceived" example:"123456789"`
|
||||||
|
// The total number of bytes transmitted by the user. This is the sum of all bytes transmitted by the peers linked to the user.
|
||||||
|
BytesTransmitted uint64 `json:"BytesTransmitted" example:"123456789"`
|
||||||
|
|
||||||
|
// PeerMetrics represents the metrics of the peers linked to the user.
|
||||||
|
PeerMetrics []PeerMetrics `json:"PeerMetrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserMetrics(srcUser *domain.User, src []domain.PeerStatus) *UserMetrics {
|
||||||
|
if srcUser == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
um := &UserMetrics{
|
||||||
|
UserIdentifier: string(srcUser.Identifier),
|
||||||
|
PeerCount: srcUser.LinkedPeerCount,
|
||||||
|
PeerMetrics: []PeerMetrics{},
|
||||||
|
|
||||||
|
BytesReceived: 0,
|
||||||
|
BytesTransmitted: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
peerMetrics := make([]PeerMetrics, len(src))
|
||||||
|
for i, peer := range src {
|
||||||
|
peerMetrics[i] = *NewPeerMetrics(&peer)
|
||||||
|
|
||||||
|
um.BytesReceived += peer.BytesReceived
|
||||||
|
um.BytesTransmitted += peer.BytesTransmitted
|
||||||
|
}
|
||||||
|
um.PeerMetrics = peerMetrics
|
||||||
|
|
||||||
|
return um
|
||||||
|
}
|
195
internal/app/api/v1/models/models_peer.go
Normal file
195
internal/app/api/v1/models/models_peer.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ExpiryDateTimeLayout = "\"2006-01-02\""
|
||||||
|
|
||||||
|
type ExpiryDate struct {
|
||||||
|
*time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON will unmarshal using 2006-01-02 layout
|
||||||
|
func (d *ExpiryDate) UnmarshalJSON(b []byte) error {
|
||||||
|
if len(b) == 0 || string(b) == "null" || string(b) == "\"\"" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(ExpiryDateTimeLayout, string(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parsed.IsZero() {
|
||||||
|
d.Time = &parsed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON will marshal using 2006-01-02 layout
|
||||||
|
func (d *ExpiryDate) MarshalJSON() ([]byte, error) {
|
||||||
|
if d == nil || d.Time == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s := d.Format(ExpiryDateTimeLayout)
|
||||||
|
return []byte(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peer represents a WireGuard peer entry.
|
||||||
|
type Peer struct {
|
||||||
|
// Identifier is the unique identifier of the peer. It is always equal to the public key of the peer.
|
||||||
|
Identifier string `json:"Identifier" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"required,len=44"`
|
||||||
|
// DisplayName is a nice display name / description for the peer.
|
||||||
|
DisplayName string `json:"DisplayName" example:"My Peer" binding:"omitempty,max=64"`
|
||||||
|
// UserIdentifier is the identifier of the user that owns the peer.
|
||||||
|
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||||
|
// InterfaceIdentifier is the identifier of the interface the peer is linked to.
|
||||||
|
InterfaceIdentifier string `json:"InterfaceIdentifier" binding:"required" example:"wg0"`
|
||||||
|
// Disabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
|
||||||
|
Disabled bool `json:"Disabled" example:"false"`
|
||||||
|
// DisabledReason is the reason why the peer has been disabled.
|
||||||
|
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:"This is a reason why the peer has been disabled."`
|
||||||
|
// ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. An expired peer is not able to connect.
|
||||||
|
ExpiresAt ExpiryDate `json:"ExpiresAt,omitempty" binding:"omitempty,datetime=2006-01-02"`
|
||||||
|
// Notes is a note field for peers.
|
||||||
|
Notes string `json:"Notes" example:"This is a note for the peer."`
|
||||||
|
|
||||||
|
// Endpoint is the endpoint address of the peer.
|
||||||
|
Endpoint ConfigOption[string] `json:"Endpoint"`
|
||||||
|
// EndpointPublicKey is the endpoint public key.
|
||||||
|
EndpointPublicKey ConfigOption[string] `json:"EndpointPublicKey"`
|
||||||
|
// AllowedIPs is a list of allowed IP subnets for the peer.
|
||||||
|
AllowedIPs ConfigOption[[]string] `json:"AllowedIPs"`
|
||||||
|
// ExtraAllowedIPs is a list of additional allowed IP subnets for the peer. These allowed IP subnets are added on the server side.
|
||||||
|
ExtraAllowedIPs []string `json:"ExtraAllowedIPs"`
|
||||||
|
// PresharedKey is the optional pre-shared Key of the peer.
|
||||||
|
PresharedKey string `json:"PresharedKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"omitempty,len=44"`
|
||||||
|
// PersistentKeepalive is the optional persistent keep-alive interval in seconds.
|
||||||
|
PersistentKeepalive ConfigOption[int] `json:"PersistentKeepalive" binding:"omitempty,gte=0"`
|
||||||
|
|
||||||
|
// PrivateKey is the private Key of the peer.
|
||||||
|
PrivateKey string `json:"PrivateKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"required,len=44"`
|
||||||
|
// PublicKey is the public Key of the server peer.
|
||||||
|
PublicKey string `json:"PublicKey" example:"TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=" binding:"omitempty,len=44"`
|
||||||
|
|
||||||
|
// Mode is the peer interface type (server, client, any).
|
||||||
|
Mode string `json:"Mode" example:"client" binding:"omitempty,oneof=server client any"`
|
||||||
|
|
||||||
|
// Addresses is a list of IP addresses in CIDR format (both IPv4 and IPv6) for the peer.
|
||||||
|
Addresses []string `json:"Addresses" example:"10.11.12.2/24" binding:"omitempty,dive,cidr"`
|
||||||
|
// CheckAliveAddress is an optional ip address or DNS name that is used for ping checks.
|
||||||
|
CheckAliveAddress string `json:"CheckAliveAddress" binding:"omitempty,ip|fqdn" example:"1.1.1.1"`
|
||||||
|
// Dns is a list of DNS servers that should be set if the peer interface is up.
|
||||||
|
Dns ConfigOption[[]string] `json:"Dns"`
|
||||||
|
// DnsSearch is the dns search option string that should be set if the peer interface is up, will be appended to Dns servers.
|
||||||
|
DnsSearch ConfigOption[[]string] `json:"DnsSearch"`
|
||||||
|
// Mtu is the device MTU of the peer.
|
||||||
|
Mtu ConfigOption[int] `json:"Mtu"`
|
||||||
|
// FirewallMark is an optional firewall mark which is used to handle peer traffic.
|
||||||
|
FirewallMark ConfigOption[uint32] `json:"FirewallMark"`
|
||||||
|
// RoutingTable is an optional routing table which is used to route peer traffic.
|
||||||
|
RoutingTable ConfigOption[string] `json:"RoutingTable"`
|
||||||
|
|
||||||
|
// PreUp is an optional action that is executed before the device is up.
|
||||||
|
PreUp ConfigOption[string] `json:"PreUp"`
|
||||||
|
// PostUp is an optional action that is executed after the device is up.
|
||||||
|
PostUp ConfigOption[string] `json:"PostUp"`
|
||||||
|
// PreDown is an optional action that is executed before the device is down.
|
||||||
|
PreDown ConfigOption[string] `json:"PreDown"`
|
||||||
|
// PostDown is an optional action that is executed after the device is down.
|
||||||
|
PostDown ConfigOption[string] `json:"PostDown"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeer(src *domain.Peer) *Peer {
|
||||||
|
return &Peer{
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
UserIdentifier: string(src.UserIdentifier),
|
||||||
|
InterfaceIdentifier: string(src.InterfaceIdentifier),
|
||||||
|
Disabled: src.IsDisabled(),
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
ExpiresAt: ExpiryDate{src.ExpiresAt},
|
||||||
|
Notes: src.Notes,
|
||||||
|
Endpoint: ConfigOptionFromDomain(src.Endpoint),
|
||||||
|
EndpointPublicKey: ConfigOptionFromDomain(src.EndpointPublicKey),
|
||||||
|
AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr),
|
||||||
|
ExtraAllowedIPs: internal.SliceString(src.ExtraAllowedIPsStr),
|
||||||
|
PresharedKey: string(src.PresharedKey),
|
||||||
|
PersistentKeepalive: ConfigOptionFromDomain(src.PersistentKeepalive),
|
||||||
|
PrivateKey: src.Interface.PrivateKey,
|
||||||
|
PublicKey: src.Interface.PublicKey,
|
||||||
|
Mode: string(src.Interface.Type),
|
||||||
|
Addresses: domain.CidrsToStringSlice(src.Interface.Addresses),
|
||||||
|
CheckAliveAddress: src.Interface.CheckAliveAddress,
|
||||||
|
Dns: StringSliceConfigOptionFromDomain(src.Interface.DnsStr),
|
||||||
|
DnsSearch: StringSliceConfigOptionFromDomain(src.Interface.DnsSearchStr),
|
||||||
|
Mtu: ConfigOptionFromDomain(src.Interface.Mtu),
|
||||||
|
FirewallMark: ConfigOptionFromDomain(src.Interface.FirewallMark),
|
||||||
|
RoutingTable: ConfigOptionFromDomain(src.Interface.RoutingTable),
|
||||||
|
PreUp: ConfigOptionFromDomain(src.Interface.PreUp),
|
||||||
|
PostUp: ConfigOptionFromDomain(src.Interface.PostUp),
|
||||||
|
PreDown: ConfigOptionFromDomain(src.Interface.PreDown),
|
||||||
|
PostDown: ConfigOptionFromDomain(src.Interface.PostDown),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeers(src []domain.Peer) []Peer {
|
||||||
|
results := make([]Peer, len(src))
|
||||||
|
for i := range src {
|
||||||
|
results[i] = *NewPeer(&src[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainPeer(src *Peer) *domain.Peer {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
cidrs, _ := domain.CidrsFromArray(src.Addresses)
|
||||||
|
|
||||||
|
res := &domain.Peer{
|
||||||
|
BaseModel: domain.BaseModel{},
|
||||||
|
Endpoint: ConfigOptionToDomain(src.Endpoint),
|
||||||
|
EndpointPublicKey: ConfigOptionToDomain(src.EndpointPublicKey),
|
||||||
|
AllowedIPsStr: StringSliceConfigOptionToDomain(src.AllowedIPs),
|
||||||
|
ExtraAllowedIPsStr: internal.SliceToString(src.ExtraAllowedIPs),
|
||||||
|
PresharedKey: domain.PreSharedKey(src.PresharedKey),
|
||||||
|
PersistentKeepalive: ConfigOptionToDomain(src.PersistentKeepalive),
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
Identifier: domain.PeerIdentifier(src.Identifier),
|
||||||
|
UserIdentifier: domain.UserIdentifier(src.UserIdentifier),
|
||||||
|
InterfaceIdentifier: domain.InterfaceIdentifier(src.InterfaceIdentifier),
|
||||||
|
Disabled: nil, // set below
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
ExpiresAt: src.ExpiresAt.Time,
|
||||||
|
Notes: src.Notes,
|
||||||
|
Interface: domain.PeerInterfaceConfig{
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PrivateKey: src.PrivateKey,
|
||||||
|
PublicKey: src.PublicKey,
|
||||||
|
},
|
||||||
|
Type: domain.InterfaceType(src.Mode),
|
||||||
|
Addresses: cidrs,
|
||||||
|
CheckAliveAddress: src.CheckAliveAddress,
|
||||||
|
DnsStr: StringSliceConfigOptionToDomain(src.Dns),
|
||||||
|
DnsSearchStr: StringSliceConfigOptionToDomain(src.DnsSearch),
|
||||||
|
Mtu: ConfigOptionToDomain(src.Mtu),
|
||||||
|
FirewallMark: ConfigOptionToDomain(src.FirewallMark),
|
||||||
|
RoutingTable: ConfigOptionToDomain(src.RoutingTable),
|
||||||
|
PreUp: ConfigOptionToDomain(src.PreUp),
|
||||||
|
PostUp: ConfigOptionToDomain(src.PostUp),
|
||||||
|
PreDown: ConfigOptionToDomain(src.PreDown),
|
||||||
|
PostDown: ConfigOptionToDomain(src.PostDown),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Disabled {
|
||||||
|
res.Disabled = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
75
internal/app/api/v1/models/models_provisioning.go
Normal file
75
internal/app/api/v1/models/models_provisioning.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "github.com/h44z/wg-portal/internal/domain"
|
||||||
|
|
||||||
|
// UserInformation represents the information about a user and its linked peers.
|
||||||
|
type UserInformation struct {
|
||||||
|
// UserIdentifier is the unique identifier of the user.
|
||||||
|
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||||
|
// PeerCount is the number of peers linked to the user.
|
||||||
|
PeerCount int `json:"PeerCount" example:"2"`
|
||||||
|
// Peers is a list of peers linked to the user.
|
||||||
|
Peers []UserInformationPeer `json:"Peers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInformationPeer represents the information about a peer.
|
||||||
|
type UserInformationPeer struct {
|
||||||
|
// Identifier is the unique identifier of the peer. It equals the public key of the peer.
|
||||||
|
Identifier string `json:"Identifier" example:"peer-1234567"`
|
||||||
|
// DisplayName is a user-defined description of the peer.
|
||||||
|
DisplayName string `json:"DisplayName" example:"My iPhone"`
|
||||||
|
// IPAddresses is a list of IP addresses in CIDR format assigned to the peer.
|
||||||
|
IpAddresses []string `json:"IpAddresses" example:"10.11.12.2/24"`
|
||||||
|
// IsDisabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
|
||||||
|
IsDisabled bool `json:"IsDisabled,omitempty" example:"true"`
|
||||||
|
|
||||||
|
// InterfaceIdentifier is the unique identifier of the WireGuard Portal device the peer is connected to.
|
||||||
|
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserInformation(user *domain.User, peers []domain.Peer) *UserInformation {
|
||||||
|
if user == nil {
|
||||||
|
return &UserInformation{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui := &UserInformation{
|
||||||
|
UserIdentifier: string(user.Identifier),
|
||||||
|
PeerCount: len(peers),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range peers {
|
||||||
|
ui.Peers = append(ui.Peers, NewUserInformationPeer(peer))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ui.Peers) == 0 {
|
||||||
|
ui.Peers = []UserInformationPeer{} // Ensure that the JSON output is an empty array instead of null.
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserInformationPeer(peer domain.Peer) UserInformationPeer {
|
||||||
|
up := UserInformationPeer{
|
||||||
|
Identifier: string(peer.Identifier),
|
||||||
|
DisplayName: peer.DisplayName,
|
||||||
|
IpAddresses: domain.CidrsToStringSlice(peer.Interface.Addresses),
|
||||||
|
IsDisabled: peer.IsDisabled(),
|
||||||
|
InterfaceIdentifier: string(peer.InterfaceIdentifier),
|
||||||
|
}
|
||||||
|
|
||||||
|
return up
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvisioningRequest represents a request to provision a new peer.
|
||||||
|
type ProvisioningRequest struct {
|
||||||
|
// InterfaceIdentifier is the identifier of the WireGuard interface the peer should be linked to.
|
||||||
|
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0" binding:"required"`
|
||||||
|
// UserIdentifier is the identifier of the user the peer should be linked to.
|
||||||
|
// If no user identifier is set, the authenticated user is used.
|
||||||
|
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||||
|
|
||||||
|
// PublicKey is the optional public key of the peer. If no public key is set, a new key pair is generated.
|
||||||
|
PublicKey string `json:"PublicKey" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"omitempty,len=44"`
|
||||||
|
// PresharedKey is the optional pre-shared key of the peer. If no pre-shared key is set, a new key is generated.
|
||||||
|
PresharedKey string `json:"PresharedKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"omitempty,len=44"`
|
||||||
|
}
|
125
internal/app/api/v1/models/models_user.go
Normal file
125
internal/app/api/v1/models/models_user.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User represents a user in the system.
|
||||||
|
type User struct {
|
||||||
|
// The unique identifier of the user.
|
||||||
|
Identifier string `json:"Identifier" binding:"required,max=64" example:"uid-1234567"`
|
||||||
|
// The email address of the user. This field is optional.
|
||||||
|
Email string `json:"Email" binding:"omitempty,email" example:"test@test.com"`
|
||||||
|
// The source of the user. This field is optional.
|
||||||
|
Source string `json:"Source" binding:"oneof=db" example:"db"`
|
||||||
|
// The name of the authentication provider. This field is read-only.
|
||||||
|
ProviderName string `json:"ProviderName,omitempty" readonly:"true" example:""`
|
||||||
|
// If this field is set, the user is an admin.
|
||||||
|
IsAdmin bool `json:"IsAdmin" binding:"required" example:"false"`
|
||||||
|
|
||||||
|
// The first name of the user. This field is optional.
|
||||||
|
Firstname string `json:"Firstname" example:"Max"`
|
||||||
|
// The last name of the user. This field is optional.
|
||||||
|
Lastname string `json:"Lastname" example:"Muster"`
|
||||||
|
// The phone number of the user. This field is optional.
|
||||||
|
Phone string `json:"Phone" example:"+1234546789"`
|
||||||
|
// The department of the user. This field is optional.
|
||||||
|
Department string `json:"Department" example:"Software Development"`
|
||||||
|
// Additional notes about the user. This field is optional.
|
||||||
|
Notes string `json:"Notes" example:"some sample notes"`
|
||||||
|
|
||||||
|
// The password of the user. This field is never populated on read operations.
|
||||||
|
Password string `json:"Password,omitempty" binding:"omitempty,min=16,max=64" example:""`
|
||||||
|
// If this field is set, the user is disabled.
|
||||||
|
Disabled bool `json:"Disabled" example:"false"`
|
||||||
|
// The reason why the user has been disabled.
|
||||||
|
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:""`
|
||||||
|
// If this field is set, the user is locked and thus unable to log in to WireGuard Portal.
|
||||||
|
Locked bool `json:"Locked" example:"false"`
|
||||||
|
// The reason why the user has been locked.
|
||||||
|
LockedReason string `json:"LockedReason" binding:"required_if=Locked true" example:""`
|
||||||
|
|
||||||
|
// The API token of the user. This field is never populated on bulk read operations.
|
||||||
|
ApiToken string `json:"ApiToken,omitempty" binding:"omitempty,min=32,max=64" example:""`
|
||||||
|
// If this field is set, the user is allowed to use the RESTful API. This field is read-only.
|
||||||
|
ApiEnabled bool `json:"ApiEnabled" readonly:"true" example:"false"`
|
||||||
|
|
||||||
|
// The number of peers linked to the user. This field is read-only.
|
||||||
|
PeerCount int `json:"PeerCount" readonly:"true" example:"2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUser(src *domain.User, exposeCredentials bool) *User {
|
||||||
|
u := &User{
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
Email: src.Email,
|
||||||
|
Source: string(src.Source),
|
||||||
|
ProviderName: src.ProviderName,
|
||||||
|
IsAdmin: src.IsAdmin,
|
||||||
|
Firstname: src.Firstname,
|
||||||
|
Lastname: src.Lastname,
|
||||||
|
Phone: src.Phone,
|
||||||
|
Department: src.Department,
|
||||||
|
Notes: src.Notes,
|
||||||
|
Password: "", // never fill password
|
||||||
|
Disabled: src.IsDisabled(),
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
Locked: src.IsLocked(),
|
||||||
|
LockedReason: src.LockedReason,
|
||||||
|
ApiToken: "", // by default, do not expose API token
|
||||||
|
ApiEnabled: src.IsApiEnabled(),
|
||||||
|
PeerCount: src.LinkedPeerCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
if exposeCredentials {
|
||||||
|
u.ApiToken = src.ApiToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUsers(src []domain.User) []User {
|
||||||
|
results := make([]User, len(src))
|
||||||
|
for i := range src {
|
||||||
|
results[i] = *NewUser(&src[i], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainUser(src *User) *domain.User {
|
||||||
|
now := time.Now()
|
||||||
|
res := &domain.User{
|
||||||
|
Identifier: domain.UserIdentifier(src.Identifier),
|
||||||
|
Email: src.Email,
|
||||||
|
Source: domain.UserSource(src.Source),
|
||||||
|
ProviderName: src.ProviderName,
|
||||||
|
IsAdmin: src.IsAdmin,
|
||||||
|
Firstname: src.Firstname,
|
||||||
|
Lastname: src.Lastname,
|
||||||
|
Phone: src.Phone,
|
||||||
|
Department: src.Department,
|
||||||
|
Notes: src.Notes,
|
||||||
|
Password: domain.PrivateString(src.Password),
|
||||||
|
Disabled: nil, // set below
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
Locked: nil, // set below
|
||||||
|
LockedReason: src.LockedReason,
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.ApiToken != "" {
|
||||||
|
res.ApiToken = src.ApiToken
|
||||||
|
res.ApiTokenCreated = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Disabled {
|
||||||
|
res.Disabled = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Locked {
|
||||||
|
res.Locked = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
@@ -22,9 +22,19 @@ type App struct {
|
|||||||
StatisticsCollector
|
StatisticsCollector
|
||||||
ConfigFileManager
|
ConfigFileManager
|
||||||
MailManager
|
MailManager
|
||||||
|
ApiV1Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config, bus evbus.MessageBus, authenticator Authenticator, users UserManager, wireGuard WireGuardManager, stats StatisticsCollector, cfgFiles ConfigFileManager, mailer MailManager) (*App, error) {
|
func New(
|
||||||
|
cfg *config.Config,
|
||||||
|
bus evbus.MessageBus,
|
||||||
|
authenticator Authenticator,
|
||||||
|
users UserManager,
|
||||||
|
wireGuard WireGuardManager,
|
||||||
|
stats StatisticsCollector,
|
||||||
|
cfgFiles ConfigFileManager,
|
||||||
|
mailer MailManager,
|
||||||
|
) (*App, error) {
|
||||||
|
|
||||||
a := &App{
|
a := &App{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
@@ -60,7 +70,7 @@ func New(cfg *config.Config, bus evbus.MessageBus, authenticator Authenticator,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Startup(ctx context.Context) error {
|
func (a *App) Startup(ctx context.Context) error {
|
||||||
|
|
||||||
a.UserManager.StartBackgroundJobs(ctx)
|
a.UserManager.StartBackgroundJobs(ctx)
|
||||||
a.StatisticsCollector.StartBackgroundJobs(ctx)
|
a.StatisticsCollector.StartBackgroundJobs(ctx)
|
||||||
a.WireGuardManager.StartBackgroundJobs(ctx)
|
a.WireGuardManager.StartBackgroundJobs(ctx)
|
||||||
@@ -119,8 +129,8 @@ func (a *App) createDefaultUser(ctx context.Context) error {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
admin, err := a.CreateUser(ctx, &domain.User{
|
admin, err := a.CreateUser(ctx, &domain.User{
|
||||||
BaseModel: domain.BaseModel{
|
BaseModel: domain.BaseModel{
|
||||||
CreatedBy: "system",
|
CreatedBy: domain.CtxSystemAdminId,
|
||||||
UpdatedBy: "system",
|
UpdatedBy: domain.CtxSystemAdminId,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
},
|
},
|
||||||
|
@@ -23,6 +23,7 @@ import (
|
|||||||
type UserManager interface {
|
type UserManager interface {
|
||||||
GetUser(context.Context, domain.UserIdentifier) (*domain.User, error)
|
GetUser(context.Context, domain.UserIdentifier) (*domain.User, error)
|
||||||
RegisterUser(ctx context.Context, user *domain.User) error
|
RegisterUser(ctx context.Context, user *domain.User) error
|
||||||
|
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Authenticator struct {
|
type Authenticator struct {
|
||||||
@@ -371,6 +372,11 @@ func (a *Authenticator) processUserInfo(
|
|||||||
}
|
}
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return nil, fmt.Errorf("registration disabled, cannot create missing user: %w", err)
|
return nil, fmt.Errorf("registration disabled, cannot create missing user: %w", err)
|
||||||
|
default:
|
||||||
|
err = a.updateExternalUser(ctx, user, userInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
@@ -400,6 +406,9 @@ func (a *Authenticator) registerNewUser(
|
|||||||
return nil, fmt.Errorf("failed to register new user: %w", err)
|
return nil, fmt.Errorf("failed to register new user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logrus.Tracef("registered user %s from external authentication provider, admin user: %t",
|
||||||
|
user.Identifier, user.IsAdmin)
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,4 +428,54 @@ func (a *Authenticator) getAuthenticatorConfig(id string) (interface{}, error) {
|
|||||||
return nil, fmt.Errorf("no configuration for Authenticator id %s", id)
|
return nil, fmt.Errorf("no configuration for Authenticator id %s", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) updateExternalUser(
|
||||||
|
ctx context.Context,
|
||||||
|
existingUser *domain.User,
|
||||||
|
userInfo *domain.AuthenticatorUserInfo,
|
||||||
|
) error {
|
||||||
|
if existingUser.IsLocked() || existingUser.IsDisabled() {
|
||||||
|
return nil // user is locked or disabled, do not update
|
||||||
|
}
|
||||||
|
|
||||||
|
isChanged := false
|
||||||
|
if existingUser.Email != userInfo.Email {
|
||||||
|
existingUser.Email = userInfo.Email
|
||||||
|
isChanged = true
|
||||||
|
}
|
||||||
|
if existingUser.Firstname != userInfo.Firstname {
|
||||||
|
existingUser.Firstname = userInfo.Firstname
|
||||||
|
isChanged = true
|
||||||
|
}
|
||||||
|
if existingUser.Lastname != userInfo.Lastname {
|
||||||
|
existingUser.Lastname = userInfo.Lastname
|
||||||
|
isChanged = true
|
||||||
|
}
|
||||||
|
if existingUser.Phone != userInfo.Phone {
|
||||||
|
existingUser.Phone = userInfo.Phone
|
||||||
|
isChanged = true
|
||||||
|
}
|
||||||
|
if existingUser.Department != userInfo.Department {
|
||||||
|
existingUser.Department = userInfo.Department
|
||||||
|
isChanged = true
|
||||||
|
}
|
||||||
|
if existingUser.IsAdmin != userInfo.IsAdmin {
|
||||||
|
existingUser.IsAdmin = userInfo.IsAdmin
|
||||||
|
isChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isChanged {
|
||||||
|
return nil // nothing to update
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := a.users.UpdateUser(ctx, existingUser)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Tracef("updated user %s with data from external authentication provider, admin user: %t",
|
||||||
|
existingUser.Identifier, existingUser.IsAdmin)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// endregion oauth authentication
|
// endregion oauth authentication
|
||||||
|
@@ -2,6 +2,7 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
"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/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LdapAuthenticator struct {
|
type LdapAuthenticator struct {
|
||||||
@@ -78,7 +80,10 @@ func (l LdapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (map[string]interface{}, error) {
|
func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (
|
||||||
|
map[string]interface{},
|
||||||
|
error,
|
||||||
|
) {
|
||||||
conn, err := internal.LdapConnect(l.cfg)
|
conn, err := internal.LdapConnect(l.cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to setup connection: %w", err)
|
return nil, fmt.Errorf("failed to setup connection: %w", err)
|
||||||
@@ -109,6 +114,11 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
|
|||||||
|
|
||||||
users := internal.LdapConvertEntries(sr, &l.cfg.FieldMap)
|
users := internal.LdapConvertEntries(sr, &l.cfg.FieldMap)
|
||||||
|
|
||||||
|
if l.cfg.LogUserInfo {
|
||||||
|
contents, _ := json.Marshal(users[0])
|
||||||
|
logrus.Tracef("User info from LDAP source %s for %s: %v", l.GetName(), userId, string(contents))
|
||||||
|
}
|
||||||
|
|
||||||
return users[0], nil
|
return users[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,12 +6,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/sirupsen/logrus"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,10 +20,16 @@ type PlainOauthAuthenticator struct {
|
|||||||
userInfoEndpoint string
|
userInfoEndpoint string
|
||||||
client *http.Client
|
client *http.Client
|
||||||
userInfoMapping config.OauthFields
|
userInfoMapping config.OauthFields
|
||||||
|
userAdminMapping *config.OauthAdminMapping
|
||||||
registrationEnabled bool
|
registrationEnabled bool
|
||||||
|
userInfoLogging bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPlainOauthAuthenticator(_ context.Context, callbackUrl string, cfg *config.OAuthProvider) (*PlainOauthAuthenticator, error) {
|
func newPlainOauthAuthenticator(
|
||||||
|
_ context.Context,
|
||||||
|
callbackUrl string,
|
||||||
|
cfg *config.OAuthProvider,
|
||||||
|
) (*PlainOauthAuthenticator, error) {
|
||||||
var provider = &PlainOauthAuthenticator{}
|
var provider = &PlainOauthAuthenticator{}
|
||||||
|
|
||||||
provider.name = cfg.ProviderName
|
provider.name = cfg.ProviderName
|
||||||
@@ -44,7 +49,9 @@ func newPlainOauthAuthenticator(_ context.Context, callbackUrl string, cfg *conf
|
|||||||
}
|
}
|
||||||
provider.userInfoEndpoint = cfg.UserInfoURL
|
provider.userInfoEndpoint = cfg.UserInfoURL
|
||||||
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
||||||
|
provider.userAdminMapping = &cfg.AdminMapping
|
||||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||||
|
provider.userInfoLogging = cfg.LogUserInfo
|
||||||
|
|
||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
@@ -65,11 +72,19 @@ func (p PlainOauthAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCo
|
|||||||
return p.cfg.AuthCodeURL(state, opts...)
|
return p.cfg.AuthCodeURL(state, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PlainOauthAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
func (p PlainOauthAuthenticator) Exchange(
|
||||||
|
ctx context.Context,
|
||||||
|
code string,
|
||||||
|
opts ...oauth2.AuthCodeOption,
|
||||||
|
) (*oauth2.Token, error) {
|
||||||
return p.cfg.Exchange(ctx, code, opts...)
|
return p.cfg.Exchange(ctx, code, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PlainOauthAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, _ string) (map[string]interface{}, error) {
|
func (p PlainOauthAuthenticator) GetUserInfo(
|
||||||
|
ctx context.Context,
|
||||||
|
token *oauth2.Token,
|
||||||
|
_ string,
|
||||||
|
) (map[string]interface{}, error) {
|
||||||
req, err := http.NewRequest("GET", p.userInfoEndpoint, nil)
|
req, err := http.NewRequest("GET", p.userInfoEndpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create user info get request: %w", err)
|
return nil, fmt.Errorf("failed to create user info get request: %w", err)
|
||||||
@@ -93,57 +108,13 @@ func (p PlainOauthAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.
|
|||||||
return nil, fmt.Errorf("failed to parse user info: %w", err)
|
return nil, fmt.Errorf("failed to parse user info: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.userInfoLogging {
|
||||||
|
logrus.Tracef("User info from OAuth source %s: %v", p.name, string(contents))
|
||||||
|
}
|
||||||
|
|
||||||
return userFields, nil
|
return userFields, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PlainOauthAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
func (p PlainOauthAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
||||||
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, p.userInfoMapping.IsAdmin, ""))
|
return parseOauthUserInfo(p.userInfoMapping, p.userAdminMapping, raw)
|
||||||
userInfo := &domain.AuthenticatorUserInfo{
|
|
||||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, p.userInfoMapping.UserIdentifier, "")),
|
|
||||||
Email: internal.MapDefaultString(raw, p.userInfoMapping.Email, ""),
|
|
||||||
Firstname: internal.MapDefaultString(raw, p.userInfoMapping.Firstname, ""),
|
|
||||||
Lastname: internal.MapDefaultString(raw, p.userInfoMapping.Lastname, ""),
|
|
||||||
Phone: internal.MapDefaultString(raw, p.userInfoMapping.Phone, ""),
|
|
||||||
Department: internal.MapDefaultString(raw, p.userInfoMapping.Department, ""),
|
|
||||||
IsAdmin: isAdmin,
|
|
||||||
}
|
|
||||||
|
|
||||||
return userInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOauthFieldMapping(f config.OauthFields) config.OauthFields {
|
|
||||||
defaultMap := config.OauthFields{
|
|
||||||
BaseFields: config.BaseFields{
|
|
||||||
UserIdentifier: "sub",
|
|
||||||
Email: "email",
|
|
||||||
Firstname: "given_name",
|
|
||||||
Lastname: "family_name",
|
|
||||||
Phone: "phone",
|
|
||||||
Department: "department",
|
|
||||||
},
|
|
||||||
IsAdmin: "admin_flag",
|
|
||||||
}
|
|
||||||
if f.UserIdentifier != "" {
|
|
||||||
defaultMap.UserIdentifier = f.UserIdentifier
|
|
||||||
}
|
|
||||||
if f.Email != "" {
|
|
||||||
defaultMap.Email = f.Email
|
|
||||||
}
|
|
||||||
if f.Firstname != "" {
|
|
||||||
defaultMap.Firstname = f.Firstname
|
|
||||||
}
|
|
||||||
if f.Lastname != "" {
|
|
||||||
defaultMap.Lastname = f.Lastname
|
|
||||||
}
|
|
||||||
if f.Phone != "" {
|
|
||||||
defaultMap.Phone = f.Phone
|
|
||||||
}
|
|
||||||
if f.Department != "" {
|
|
||||||
defaultMap.Department = f.Department
|
|
||||||
}
|
|
||||||
if f.IsAdmin != "" {
|
|
||||||
defaultMap.IsAdmin = f.IsAdmin
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultMap
|
|
||||||
}
|
}
|
||||||
|
@@ -2,14 +2,14 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"github.com/h44z/wg-portal/internal"
|
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,15 +19,22 @@ type OidcAuthenticator struct {
|
|||||||
verifier *oidc.IDTokenVerifier
|
verifier *oidc.IDTokenVerifier
|
||||||
cfg *oauth2.Config
|
cfg *oauth2.Config
|
||||||
userInfoMapping config.OauthFields
|
userInfoMapping config.OauthFields
|
||||||
|
userAdminMapping *config.OauthAdminMapping
|
||||||
registrationEnabled bool
|
registrationEnabled bool
|
||||||
|
userInfoLogging bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOidcAuthenticator(ctx context.Context, callbackUrl string, cfg *config.OpenIDConnectProvider) (*OidcAuthenticator, error) {
|
func newOidcAuthenticator(
|
||||||
|
_ context.Context,
|
||||||
|
callbackUrl string,
|
||||||
|
cfg *config.OpenIDConnectProvider,
|
||||||
|
) (*OidcAuthenticator, error) {
|
||||||
var err error
|
var err error
|
||||||
var provider = &OidcAuthenticator{}
|
var provider = &OidcAuthenticator{}
|
||||||
|
|
||||||
provider.name = cfg.ProviderName
|
provider.name = cfg.ProviderName
|
||||||
provider.provider, err = oidc.NewProvider(context.Background(), cfg.BaseUrl) // use new context here, see https://github.com/coreos/go-oidc/issues/339
|
provider.provider, err = oidc.NewProvider(context.Background(),
|
||||||
|
cfg.BaseUrl) // use new context here, see https://github.com/coreos/go-oidc/issues/339
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create new oidc provider: %w", err)
|
return nil, fmt.Errorf("failed to create new oidc provider: %w", err)
|
||||||
}
|
}
|
||||||
@@ -45,7 +52,9 @@ func newOidcAuthenticator(ctx context.Context, callbackUrl string, cfg *config.O
|
|||||||
Scopes: scopes,
|
Scopes: scopes,
|
||||||
}
|
}
|
||||||
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
||||||
|
provider.userAdminMapping = &cfg.AdminMapping
|
||||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||||
|
provider.userInfoLogging = cfg.LogUserInfo
|
||||||
|
|
||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
@@ -66,11 +75,17 @@ func (o OidcAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCodeOpti
|
|||||||
return o.cfg.AuthCodeURL(state, opts...)
|
return o.cfg.AuthCodeURL(state, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (
|
||||||
|
*oauth2.Token,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
return o.cfg.Exchange(ctx, code, opts...)
|
return o.cfg.Exchange(ctx, code, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (map[string]interface{}, error) {
|
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (
|
||||||
|
map[string]interface{},
|
||||||
|
error,
|
||||||
|
) {
|
||||||
rawIDToken, ok := token.Extra("id_token").(string)
|
rawIDToken, ok := token.Extra("id_token").(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("token does not contain id_token")
|
return nil, errors.New("token does not contain id_token")
|
||||||
@@ -88,20 +103,14 @@ func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token,
|
|||||||
return nil, fmt.Errorf("failed to parse extra claims: %w", err)
|
return nil, fmt.Errorf("failed to parse extra claims: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.userInfoLogging {
|
||||||
|
contents, _ := json.Marshal(tokenFields)
|
||||||
|
logrus.Tracef("User info from OIDC source %s: %v", o.name, string(contents))
|
||||||
|
}
|
||||||
|
|
||||||
return tokenFields, nil
|
return tokenFields, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o OidcAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
func (o OidcAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
||||||
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, o.userInfoMapping.IsAdmin, ""))
|
return parseOauthUserInfo(o.userInfoMapping, o.userAdminMapping, raw)
|
||||||
userInfo := &domain.AuthenticatorUserInfo{
|
|
||||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, o.userInfoMapping.UserIdentifier, "")),
|
|
||||||
Email: internal.MapDefaultString(raw, o.userInfoMapping.Email, ""),
|
|
||||||
Firstname: internal.MapDefaultString(raw, o.userInfoMapping.Firstname, ""),
|
|
||||||
Lastname: internal.MapDefaultString(raw, o.userInfoMapping.Lastname, ""),
|
|
||||||
Phone: internal.MapDefaultString(raw, o.userInfoMapping.Phone, ""),
|
|
||||||
Department: internal.MapDefaultString(raw, o.userInfoMapping.Department, ""),
|
|
||||||
IsAdmin: isAdmin,
|
|
||||||
}
|
|
||||||
|
|
||||||
return userInfo, nil
|
|
||||||
}
|
}
|
||||||
|
88
internal/app/auth/oauth_common.go
Normal file
88
internal/app/auth/oauth_common.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseOauthUserInfo parses the raw user info from the oauth provider and maps it to the internal user info struct
|
||||||
|
func parseOauthUserInfo(
|
||||||
|
mapping config.OauthFields,
|
||||||
|
adminMapping *config.OauthAdminMapping,
|
||||||
|
raw map[string]interface{},
|
||||||
|
) (*domain.AuthenticatorUserInfo, error) {
|
||||||
|
var isAdmin bool
|
||||||
|
|
||||||
|
// first try to match the is_admin field against the given regex
|
||||||
|
if mapping.IsAdmin != "" {
|
||||||
|
re := adminMapping.GetAdminValueRegex()
|
||||||
|
if re.MatchString(strings.TrimSpace(internal.MapDefaultString(raw, mapping.IsAdmin, ""))) {
|
||||||
|
isAdmin = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// next try to parse the user's groups
|
||||||
|
if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" {
|
||||||
|
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
|
||||||
|
re := adminMapping.GetAdminGroupRegex()
|
||||||
|
for _, group := range userGroups {
|
||||||
|
if re.MatchString(strings.TrimSpace(group)) {
|
||||||
|
isAdmin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo := &domain.AuthenticatorUserInfo{
|
||||||
|
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
|
||||||
|
Email: internal.MapDefaultString(raw, mapping.Email, ""),
|
||||||
|
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
|
||||||
|
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
|
||||||
|
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),
|
||||||
|
Department: internal.MapDefaultString(raw, mapping.Department, ""),
|
||||||
|
IsAdmin: isAdmin,
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOauthFieldMapping returns the default field mapping for the oauth provider
|
||||||
|
func getOauthFieldMapping(f config.OauthFields) config.OauthFields {
|
||||||
|
defaultMap := config.OauthFields{
|
||||||
|
BaseFields: config.BaseFields{
|
||||||
|
UserIdentifier: "sub",
|
||||||
|
Email: "email",
|
||||||
|
Firstname: "given_name",
|
||||||
|
Lastname: "family_name",
|
||||||
|
Phone: "phone",
|
||||||
|
Department: "department",
|
||||||
|
},
|
||||||
|
IsAdmin: "admin_flag",
|
||||||
|
}
|
||||||
|
if f.UserIdentifier != "" {
|
||||||
|
defaultMap.UserIdentifier = f.UserIdentifier
|
||||||
|
}
|
||||||
|
if f.Email != "" {
|
||||||
|
defaultMap.Email = f.Email
|
||||||
|
}
|
||||||
|
if f.Firstname != "" {
|
||||||
|
defaultMap.Firstname = f.Firstname
|
||||||
|
}
|
||||||
|
if f.Lastname != "" {
|
||||||
|
defaultMap.Lastname = f.Lastname
|
||||||
|
}
|
||||||
|
if f.Phone != "" {
|
||||||
|
defaultMap.Phone = f.Phone
|
||||||
|
}
|
||||||
|
if f.Department != "" {
|
||||||
|
defaultMap.Department = f.Department
|
||||||
|
}
|
||||||
|
if f.IsAdmin != "" {
|
||||||
|
defaultMap.IsAdmin = f.IsAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultMap
|
||||||
|
}
|
@@ -1,6 +1,8 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
const TopicUserCreated = "user:created"
|
const TopicUserCreated = "user:created"
|
||||||
|
const TopicUserApiEnabled = "user:api:enabled"
|
||||||
|
const TopicUserApiDisabled = "user:api:disabled"
|
||||||
const TopicUserRegistered = "user:registered"
|
const TopicUserRegistered = "user:registered"
|
||||||
const TopicUserDisabled = "user:disabled"
|
const TopicUserDisabled = "user:disabled"
|
||||||
const TopicUserEnabled = "user:enabled"
|
const TopicUserEnabled = "user:enabled"
|
||||||
|
@@ -98,8 +98,8 @@ func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
newUser := domain.User{
|
newUser := domain.User{
|
||||||
BaseModel: domain.BaseModel{
|
BaseModel: domain.BaseModel{
|
||||||
CreatedBy: "v1migrator",
|
CreatedBy: domain.CtxSystemV1Migrator,
|
||||||
UpdatedBy: "v1migrator",
|
UpdatedBy: domain.CtxSystemV1Migrator,
|
||||||
CreatedAt: oldUser.CreatedAt,
|
CreatedAt: oldUser.CreatedAt,
|
||||||
UpdatedAt: oldUser.UpdatedAt,
|
UpdatedAt: oldUser.UpdatedAt,
|
||||||
},
|
},
|
||||||
@@ -173,8 +173,8 @@ func migrateV1Interfaces(oldDb, newDb *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
newInterface := domain.Interface{
|
newInterface := domain.Interface{
|
||||||
BaseModel: domain.BaseModel{
|
BaseModel: domain.BaseModel{
|
||||||
CreatedBy: "v1migrator",
|
CreatedBy: domain.CtxSystemV1Migrator,
|
||||||
UpdatedBy: "v1migrator",
|
UpdatedBy: domain.CtxSystemV1Migrator,
|
||||||
CreatedAt: oldDevice.CreatedAt,
|
CreatedAt: oldDevice.CreatedAt,
|
||||||
UpdatedAt: oldDevice.UpdatedAt,
|
UpdatedAt: oldDevice.UpdatedAt,
|
||||||
},
|
},
|
||||||
@@ -299,8 +299,8 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
user = domain.User{
|
user = domain.User{
|
||||||
BaseModel: domain.BaseModel{
|
BaseModel: domain.BaseModel{
|
||||||
CreatedBy: "v1migrator",
|
CreatedBy: domain.CtxSystemV1Migrator,
|
||||||
UpdatedBy: "v1migrator",
|
UpdatedBy: domain.CtxSystemV1Migrator,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
},
|
},
|
||||||
@@ -322,8 +322,8 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
newPeer := domain.Peer{
|
newPeer := domain.Peer{
|
||||||
BaseModel: domain.BaseModel{
|
BaseModel: domain.BaseModel{
|
||||||
CreatedBy: "v1migrator",
|
CreatedBy: domain.CtxSystemV1Migrator,
|
||||||
UpdatedBy: "v1migrator",
|
UpdatedBy: domain.CtxSystemV1Migrator,
|
||||||
CreatedAt: oldPeer.CreatedAt,
|
CreatedAt: oldPeer.CreatedAt,
|
||||||
UpdatedAt: oldPeer.UpdatedAt,
|
UpdatedAt: oldPeer.UpdatedAt,
|
||||||
},
|
},
|
||||||
|
@@ -2,8 +2,9 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Authenticator interface {
|
type Authenticator interface {
|
||||||
@@ -23,6 +24,8 @@ type UserManager interface {
|
|||||||
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||||
|
ActivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WireGuardManager interface {
|
type WireGuardManager interface {
|
||||||
@@ -43,7 +46,11 @@ type WireGuardManager interface {
|
|||||||
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
|
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
|
||||||
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||||
CreateMultiplePeers(ctx context.Context, id domain.InterfaceIdentifier, r *domain.PeerCreationRequest) ([]domain.Peer, error)
|
CreateMultiplePeers(
|
||||||
|
ctx context.Context,
|
||||||
|
id domain.InterfaceIdentifier,
|
||||||
|
r *domain.PeerCreationRequest,
|
||||||
|
) ([]domain.Peer, error)
|
||||||
UpdatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
UpdatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||||
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||||
ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error
|
ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error
|
||||||
@@ -63,3 +70,7 @@ type ConfigFileManager interface {
|
|||||||
type MailManager interface {
|
type MailManager interface {
|
||||||
SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error
|
SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ApiV1Manager interface {
|
||||||
|
ApiV1GetUsers(ctx context.Context) ([]domain.User, error)
|
||||||
|
}
|
||||||
|
@@ -2,15 +2,21 @@ package users
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
"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"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func convertRawLdapUser(providerName string, rawUser map[string]any, fields *config.LdapFields, adminGroupDN *ldap.DN) (*domain.User, error) {
|
func convertRawLdapUser(
|
||||||
|
providerName string,
|
||||||
|
rawUser map[string]any,
|
||||||
|
fields *config.LdapFields,
|
||||||
|
adminGroupDN *ldap.DN,
|
||||||
|
) (*domain.User, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
isAdmin, err := internal.LdapIsMemberOf(rawUser[fields.GroupMembership].([][]byte), adminGroupDN)
|
isAdmin, err := internal.LdapIsMemberOf(rawUser[fields.GroupMembership].([][]byte), adminGroupDN)
|
||||||
@@ -20,8 +26,8 @@ func convertRawLdapUser(providerName string, rawUser map[string]any, fields *con
|
|||||||
|
|
||||||
return &domain.User{
|
return &domain.User{
|
||||||
BaseModel: domain.BaseModel{
|
BaseModel: domain.BaseModel{
|
||||||
CreatedBy: "ldap_sync",
|
CreatedBy: domain.CtxSystemLdapSyncer,
|
||||||
UpdatedBy: "ldap_sync",
|
UpdatedBy: domain.CtxSystemLdapSyncer,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
},
|
},
|
||||||
|
@@ -2,11 +2,13 @@ package users
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserDatabaseRepo interface {
|
type UserDatabaseRepo interface {
|
||||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||||
FindUsers(ctx context.Context, search string) ([]domain.User, error)
|
FindUsers(ctx context.Context, search string) ([]domain.User, error)
|
||||||
SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error
|
SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
"github.com/h44z/wg-portal/internal"
|
||||||
@@ -101,7 +102,7 @@ func (m Manager) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain
|
|||||||
|
|
||||||
user, err := m.users.GetUser(ctx, id)
|
user, err := m.users.GetUser(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to load peer %s: %w", id, err)
|
return nil, fmt.Errorf("unable to load user %s: %w", id, err)
|
||||||
}
|
}
|
||||||
peers, _ := m.peers.GetUserPeers(ctx, id) // ignore error, list will be empty in error case
|
peers, _ := m.peers.GetUserPeers(ctx, id) // ignore error, list will be empty in error case
|
||||||
|
|
||||||
@@ -110,6 +111,24 @@ func (m Manager) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Manager) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
|
|
||||||
|
user, err := m.users.GetUserByEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to load user for email %s: %w", email, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, user.Identifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, _ := m.peers.GetUserPeers(ctx, user.Identifier) // ignore error, list will be empty in error case
|
||||||
|
|
||||||
|
user.LinkedPeerCount = len(peers)
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m Manager) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
func (m Manager) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -193,7 +212,7 @@ func (m Manager) CreateUser(ctx context.Context, user *domain.User) (*domain.Use
|
|||||||
return nil, fmt.Errorf("unable to load existing user %s: %w", user.Identifier, err)
|
return nil, fmt.Errorf("unable to load existing user %s: %w", user.Identifier, err)
|
||||||
}
|
}
|
||||||
if existingUser != nil {
|
if existingUser != nil {
|
||||||
return nil, fmt.Errorf("user %s already exists", user.Identifier)
|
return nil, errors.Join(fmt.Errorf("user %s already exists", user.Identifier), domain.ErrDuplicateEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.validateCreation(ctx, user); err != nil {
|
if err := m.validateCreation(ctx, user); err != nil {
|
||||||
@@ -240,6 +259,59 @@ func (m Manager) DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Manager) ActivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||||
|
user, err := m.users.GetUser(ctx, id)
|
||||||
|
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||||
|
return nil, fmt.Errorf("unable to find user %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.validateApiChange(ctx, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
user.ApiToken = uuid.New().String()
|
||||||
|
user.ApiTokenCreated = &now
|
||||||
|
|
||||||
|
err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||||
|
user.CopyCalculatedAttributes(u)
|
||||||
|
return user, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("update failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.bus.Publish(app.TopicUserApiEnabled, user)
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Manager) DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||||
|
user, err := m.users.GetUser(ctx, id)
|
||||||
|
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||||
|
return nil, fmt.Errorf("unable to find user %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.validateApiChange(ctx, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.ApiToken = ""
|
||||||
|
user.ApiTokenCreated = nil
|
||||||
|
|
||||||
|
err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||||
|
user.CopyCalculatedAttributes(u)
|
||||||
|
return user, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("update failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.bus.Publish(app.TopicUserApiDisabled, user)
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m Manager) validateModifications(ctx context.Context, old, new *domain.User) error {
|
func (m Manager) validateModifications(ctx context.Context, old, new *domain.User) error {
|
||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
@@ -247,28 +319,28 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use
|
|||||||
return fmt.Errorf("insufficient permissions")
|
return fmt.Errorf("insufficient permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := old.EditAllowed(new); err != nil {
|
if err := old.EditAllowed(new); err != nil && currentUser.Id != domain.SystemAdminContextUserInfo().Id {
|
||||||
return fmt.Errorf("no access: %w", err)
|
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := old.CanChangePassword(); err != nil && string(new.Password) != "" {
|
if err := old.CanChangePassword(); err != nil && string(new.Password) != "" {
|
||||||
return fmt.Errorf("no access: %w", err)
|
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentUser.Id == old.Identifier && old.IsAdmin && !new.IsAdmin {
|
if currentUser.Id == old.Identifier && old.IsAdmin && !new.IsAdmin {
|
||||||
return fmt.Errorf("cannot remove own admin rights")
|
return fmt.Errorf("cannot remove own admin rights: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentUser.Id == old.Identifier && new.IsDisabled() {
|
if currentUser.Id == old.Identifier && new.IsDisabled() {
|
||||||
return fmt.Errorf("cannot disable own user")
|
return fmt.Errorf("cannot disable own user: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentUser.Id == old.Identifier && new.IsLocked() {
|
if currentUser.Id == old.Identifier && new.IsLocked() {
|
||||||
return fmt.Errorf("cannot lock own user")
|
return fmt.Errorf("cannot lock own user: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if old.Source != new.Source {
|
if old.Source != new.Source {
|
||||||
return fmt.Errorf("cannot change user source")
|
return fmt.Errorf("cannot change user source: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -282,19 +354,32 @@ func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if new.Identifier == "" {
|
if new.Identifier == "" {
|
||||||
return fmt.Errorf("invalid user identifier")
|
return fmt.Errorf("invalid user identifier: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if new.Identifier == "all" { // the all user identifier collides with the rest api routes
|
if new.Identifier == "all" { // the 'all' user identifier collides with the rest api routes
|
||||||
return fmt.Errorf("reserved user identifier")
|
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if new.Identifier == "new" { // the 'new' user identifier collides with the rest api routes
|
||||||
|
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if new.Identifier == "id" { // the 'id' user identifier collides with the rest api routes
|
||||||
|
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if new.Identifier == domain.CtxSystemAdminId || new.Identifier == domain.CtxUnknownUserId {
|
||||||
|
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if new.Source != domain.UserSourceDatabase {
|
if new.Source != domain.UserSourceDatabase {
|
||||||
return fmt.Errorf("invalid user source: %s", new.Source)
|
return fmt.Errorf("invalid user source: %s, only %s is allowed: %w",
|
||||||
|
new.Source, domain.UserSourceDatabase, domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(new.Password) == "" {
|
if string(new.Password) == "" {
|
||||||
return fmt.Errorf("invalid password")
|
return fmt.Errorf("invalid password: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -304,15 +389,25 @@ func (m Manager) validateDeletion(ctx context.Context, del *domain.User) error {
|
|||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin {
|
||||||
return fmt.Errorf("insufficient permissions")
|
return domain.ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := del.DeleteAllowed(); err != nil {
|
if err := del.DeleteAllowed(); err != nil {
|
||||||
return fmt.Errorf("no access: %w", err)
|
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentUser.Id == del.Identifier {
|
if currentUser.Id == del.Identifier {
|
||||||
return fmt.Errorf("cannot delete own user")
|
return fmt.Errorf("cannot delete own user: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Manager) validateApiChange(ctx context.Context, user *domain.User) error {
|
||||||
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
|
if currentUser.Id != user.Identifier {
|
||||||
|
return fmt.Errorf("cannot change API access of user: %w", domain.ErrNoPermission)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -418,7 +513,7 @@ func (m Manager) updateLdapUsers(
|
|||||||
|
|
||||||
err := m.users.SaveUser(tctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
err := m.users.SaveUser(tctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||||
u.UpdatedAt = time.Now()
|
u.UpdatedAt = time.Now()
|
||||||
u.UpdatedBy = "ldap_sync"
|
u.UpdatedBy = domain.CtxSystemLdapSyncer
|
||||||
u.Email = user.Email
|
u.Email = user.Email
|
||||||
u.Firstname = user.Firstname
|
u.Firstname = user.Firstname
|
||||||
u.Lastname = user.Lastname
|
u.Lastname = user.Lastname
|
||||||
@@ -472,10 +567,13 @@ func (m Manager) disableMissingLdapUsers(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
user.Disabled = &now
|
||||||
|
user.DisabledReason = domain.DisabledReasonLdapMissing
|
||||||
|
|
||||||
err := m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
err := m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||||
now := time.Now()
|
u.Disabled = user.Disabled
|
||||||
u.Disabled = &now
|
u.DisabledReason = user.DisabledReason
|
||||||
u.DisabledReason = "missing in ldap"
|
|
||||||
return u, nil
|
return u, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -357,7 +357,7 @@ func (m Manager) CreateInterface(ctx context.Context, in *domain.Interface) (*do
|
|||||||
return nil, fmt.Errorf("unable to load existing interface %s: %w", in.Identifier, err)
|
return nil, fmt.Errorf("unable to load existing interface %s: %w", in.Identifier, err)
|
||||||
}
|
}
|
||||||
if existingInterface != nil {
|
if existingInterface != nil {
|
||||||
return nil, fmt.Errorf("interface %s already exists", in.Identifier)
|
return nil, fmt.Errorf("interface %s already exists: %w", in.Identifier, domain.ErrDuplicateEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.validateInterfaceCreation(ctx, existingInterface, in); err != nil {
|
if err := m.validateInterfaceCreation(ctx, existingInterface, in); err != nil {
|
||||||
@@ -705,8 +705,8 @@ func (m Manager) importInterface(ctx context.Context, in *domain.PhysicalInterfa
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
iface := domain.ConvertPhysicalInterface(in)
|
iface := domain.ConvertPhysicalInterface(in)
|
||||||
iface.BaseModel = domain.BaseModel{
|
iface.BaseModel = domain.BaseModel{
|
||||||
CreatedBy: "importer",
|
CreatedBy: domain.CtxSystemWgImporter,
|
||||||
UpdatedBy: "importer",
|
UpdatedBy: domain.CtxSystemWgImporter,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
@@ -742,8 +742,8 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
peer := domain.ConvertPhysicalPeer(p)
|
peer := domain.ConvertPhysicalPeer(p)
|
||||||
peer.BaseModel = domain.BaseModel{
|
peer.BaseModel = domain.BaseModel{
|
||||||
CreatedBy: "importer",
|
CreatedBy: domain.CtxSystemWgImporter,
|
||||||
UpdatedBy: "importer",
|
UpdatedBy: domain.CtxSystemWgImporter,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
@@ -825,6 +825,13 @@ func (m Manager) validateInterfaceCreation(ctx context.Context, old, new *domain
|
|||||||
return fmt.Errorf("insufficient permissions")
|
return fmt.Errorf("insufficient permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate public key if it is set
|
||||||
|
if new.PublicKey != "" && new.PrivateKey != "" {
|
||||||
|
if domain.PublicKeyFromPrivateKey(new.PrivateKey) != new.PublicKey {
|
||||||
|
return fmt.Errorf("invalid public key for given privatekey: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@@ -34,9 +33,9 @@ func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdenti
|
|||||||
}
|
}
|
||||||
|
|
||||||
peer.UserIdentifier = userId
|
peer.UserIdentifier = userId
|
||||||
peer.DisplayName = fmt.Sprintf("Default Peer %s", internal.TruncateString(string(peer.Identifier), 8))
|
|
||||||
peer.Notes = fmt.Sprintf("Default peer created for user %s", userId)
|
peer.Notes = fmt.Sprintf("Default peer created for user %s", userId)
|
||||||
peer.AutomaticallyCreated = true
|
peer.AutomaticallyCreated = true
|
||||||
|
peer.GenerateDisplayName("Default")
|
||||||
|
|
||||||
newPeers = append(newPeers, *peer)
|
newPeers = append(newPeers, *peer)
|
||||||
}
|
}
|
||||||
@@ -108,7 +107,6 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
|
|||||||
ExtraAllowedIPsStr: "",
|
ExtraAllowedIPsStr: "",
|
||||||
PresharedKey: pk,
|
PresharedKey: pk,
|
||||||
PersistentKeepalive: domain.NewConfigOption(iface.PeerDefPersistentKeepalive, true),
|
PersistentKeepalive: domain.NewConfigOption(iface.PeerDefPersistentKeepalive, true),
|
||||||
DisplayName: fmt.Sprintf("Peer %s", internal.TruncateString(string(peerId), 8)),
|
|
||||||
Identifier: peerId,
|
Identifier: peerId,
|
||||||
UserIdentifier: currentUser.Id,
|
UserIdentifier: currentUser.Id,
|
||||||
InterfaceIdentifier: iface.Identifier,
|
InterfaceIdentifier: iface.Identifier,
|
||||||
@@ -132,6 +130,7 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
|
|||||||
PostDown: domain.NewConfigOption(iface.PeerDefPostDown, true),
|
PostDown: domain.NewConfigOption(iface.PeerDefPostDown, true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
freshPeer.GenerateDisplayName("")
|
||||||
|
|
||||||
return freshPeer, nil
|
return freshPeer, nil
|
||||||
}
|
}
|
||||||
@@ -159,7 +158,7 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
|
|||||||
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
|
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
|
||||||
}
|
}
|
||||||
if existingPeer != nil {
|
if existingPeer != nil {
|
||||||
return nil, fmt.Errorf("peer %s already exists", peer.Identifier)
|
return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil {
|
if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil {
|
||||||
@@ -234,6 +233,15 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
|
|||||||
if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
|
if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
|
||||||
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier
|
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier
|
||||||
|
|
||||||
|
// check for already existing peer with new identifier
|
||||||
|
duplicatePeer, err := m.db.GetPeer(ctx, peer.Identifier)
|
||||||
|
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||||
|
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
|
||||||
|
}
|
||||||
|
if duplicatePeer != nil {
|
||||||
|
return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
|
||||||
|
}
|
||||||
|
|
||||||
// delete old peer
|
// delete old peer
|
||||||
err = m.DeletePeer(ctx, existingPeer.Identifier)
|
err = m.DeletePeer(ctx, existingPeer.Identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -431,7 +439,7 @@ func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain
|
|||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin {
|
||||||
return fmt.Errorf("insufficient permissions")
|
return domain.ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -441,11 +449,16 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
|
|||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if new.Identifier == "" {
|
if new.Identifier == "" {
|
||||||
return fmt.Errorf("invalid peer identifier")
|
return fmt.Errorf("invalid peer identifier: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin {
|
||||||
return fmt.Errorf("insufficient permissions")
|
return domain.ErrNoPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := m.db.GetInterface(ctx, new.InterfaceIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid interface: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -455,7 +468,7 @@ func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) err
|
|||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin {
|
||||||
return fmt.Errorf("insufficient permissions")
|
return domain.ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
@@ -23,7 +25,67 @@ type BaseFields struct {
|
|||||||
|
|
||||||
type OauthFields struct {
|
type OauthFields struct {
|
||||||
BaseFields `yaml:",inline"`
|
BaseFields `yaml:",inline"`
|
||||||
IsAdmin string `yaml:"is_admin"` // If the value is "true", the user is an admin.
|
IsAdmin string `yaml:"is_admin"` // If the value is "true", the user is an admin.
|
||||||
|
UserGroups string `yaml:"user_groups"` // This value specifies the claim name that contains the users groups.
|
||||||
|
}
|
||||||
|
|
||||||
|
// OauthAdminMapping contains all necessary information to extract information about administrative privileges
|
||||||
|
// from the user info fields.
|
||||||
|
//
|
||||||
|
// WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression.
|
||||||
|
// Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the
|
||||||
|
// `user_group` claim.
|
||||||
|
// If one of the cases evaluates to true, the user is granted admin rights.
|
||||||
|
type OauthAdminMapping struct {
|
||||||
|
// If the regex specified in that field matches the contents of the is_admin field, the user is an admin.
|
||||||
|
AdminValueRegex string `yaml:"admin_value_regex"`
|
||||||
|
|
||||||
|
// If any of the groups listed in the groups field matches the group specified in the admin_group_regex field, ]
|
||||||
|
// the user is an admin.
|
||||||
|
AdminGroupRegex string `yaml:"admin_group_regex"`
|
||||||
|
|
||||||
|
// internal cache fields
|
||||||
|
|
||||||
|
adminValueRegex *regexp.Regexp
|
||||||
|
adminGroupRegex *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OauthAdminMapping) GetAdminValueRegex() *regexp.Regexp {
|
||||||
|
if o.adminValueRegex != nil {
|
||||||
|
return o.adminValueRegex // return cached value
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.AdminValueRegex == "" {
|
||||||
|
o.adminValueRegex = regexp.MustCompile("^true$") // default value is "true"
|
||||||
|
return o.adminValueRegex
|
||||||
|
}
|
||||||
|
|
||||||
|
adminRegex, err := regexp.Compile(o.AdminValueRegex)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("failed to compile admin_value_regex: %v", err)
|
||||||
|
}
|
||||||
|
o.adminValueRegex = adminRegex
|
||||||
|
|
||||||
|
return o.adminValueRegex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OauthAdminMapping) GetAdminGroupRegex() *regexp.Regexp {
|
||||||
|
if o.adminGroupRegex != nil {
|
||||||
|
return o.adminGroupRegex // return cached value
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.AdminGroupRegex == "" {
|
||||||
|
o.adminGroupRegex = regexp.MustCompile("^wg_portal_default_admin_group$") // default value is "wg_portal_default_admin_group"
|
||||||
|
return o.adminGroupRegex
|
||||||
|
}
|
||||||
|
|
||||||
|
groupRegex, err := regexp.Compile(o.AdminGroupRegex)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("failed to compile admin_group_regex: %v", err)
|
||||||
|
}
|
||||||
|
o.adminGroupRegex = groupRegex
|
||||||
|
|
||||||
|
return o.adminGroupRegex
|
||||||
}
|
}
|
||||||
|
|
||||||
type LdapFields struct {
|
type LdapFields struct {
|
||||||
@@ -58,6 +120,9 @@ type LdapProvider struct {
|
|||||||
|
|
||||||
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
|
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
|
||||||
RegistrationEnabled bool `yaml:"registration_enabled"`
|
RegistrationEnabled bool `yaml:"registration_enabled"`
|
||||||
|
|
||||||
|
// If LogUserInfo is set to true, the user info retrieved from the LDAP provider will be logged in trace level.
|
||||||
|
LogUserInfo bool `yaml:"log_user_info"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenIDConnectProvider struct {
|
type OpenIDConnectProvider struct {
|
||||||
@@ -81,8 +146,15 @@ type OpenIDConnectProvider struct {
|
|||||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||||
FieldMap OauthFields `yaml:"field_map"`
|
FieldMap OauthFields `yaml:"field_map"`
|
||||||
|
|
||||||
|
// AdminMapping contains all necessary information to extract information about administrative privileges
|
||||||
|
// from the user info fields.
|
||||||
|
AdminMapping OauthAdminMapping `yaml:"admin_mapping"`
|
||||||
|
|
||||||
// If RegistrationEnabled is set to true, missing users will be created in the database
|
// If RegistrationEnabled is set to true, missing users will be created in the database
|
||||||
RegistrationEnabled bool `yaml:"registration_enabled"`
|
RegistrationEnabled bool `yaml:"registration_enabled"`
|
||||||
|
|
||||||
|
// If LogUserInfo is set to true, the user info retrieved from the OIDC provider will be logged in trace level.
|
||||||
|
LogUserInfo bool `yaml:"log_user_info"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthProvider struct {
|
type OAuthProvider struct {
|
||||||
@@ -108,6 +180,13 @@ type OAuthProvider struct {
|
|||||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||||
FieldMap OauthFields `yaml:"field_map"`
|
FieldMap OauthFields `yaml:"field_map"`
|
||||||
|
|
||||||
|
// AdminMapping contains all necessary information to extract information about administrative privileges
|
||||||
|
// from the user info fields.
|
||||||
|
AdminMapping OauthAdminMapping `yaml:"admin_mapping"`
|
||||||
|
|
||||||
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
|
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
|
||||||
RegistrationEnabled bool `yaml:"registration_enabled"`
|
RegistrationEnabled bool `yaml:"registration_enabled"`
|
||||||
|
|
||||||
|
// If LogUserInfo is set to true, the user info retrieved from the OAuth provider will be logged in trace level.
|
||||||
|
LogUserInfo bool `yaml:"log_user_info"`
|
||||||
}
|
}
|
||||||
|
@@ -39,6 +39,7 @@ type Config struct {
|
|||||||
ExpiryCheckInterval time.Duration `yaml:"expiry_check_interval"`
|
ExpiryCheckInterval time.Duration `yaml:"expiry_check_interval"`
|
||||||
RulePrioOffset int `yaml:"rule_prio_offset"`
|
RulePrioOffset int `yaml:"rule_prio_offset"`
|
||||||
RouteTableOffset int `yaml:"route_table_offset"`
|
RouteTableOffset int `yaml:"route_table_offset"`
|
||||||
|
ApiAdminOnly bool `yaml:"api_admin_only"` // if true, only admin users can access the API
|
||||||
} `yaml:"advanced"`
|
} `yaml:"advanced"`
|
||||||
|
|
||||||
Statistics struct {
|
Statistics struct {
|
||||||
@@ -126,6 +127,7 @@ func defaultConfig() *Config {
|
|||||||
cfg.Advanced.ExpiryCheckInterval = 15 * time.Minute
|
cfg.Advanced.ExpiryCheckInterval = 15 * time.Minute
|
||||||
cfg.Advanced.RulePrioOffset = 20000
|
cfg.Advanced.RulePrioOffset = 20000
|
||||||
cfg.Advanced.RouteTableOffset = 20000
|
cfg.Advanced.RouteTableOffset = 20000
|
||||||
|
cfg.Advanced.ApiAdminOnly = true
|
||||||
|
|
||||||
cfg.Statistics.UsePingChecks = true
|
cfg.Statistics.UsePingChecks = true
|
||||||
cfg.Statistics.PingCheckWorkers = 10
|
cfg.Statistics.PingCheckWorkers = 10
|
||||||
|
@@ -3,6 +3,7 @@ package domain
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -11,8 +12,11 @@ import (
|
|||||||
const CtxUserInfo = "userInfo"
|
const CtxUserInfo = "userInfo"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CtxSystemAdminId = "_WG_SYS_ADMIN_"
|
CtxSystemAdminId = "_WG_SYS_ADMIN_"
|
||||||
CtxUnknownUserId = "_WG_SYS_UNKNOWN_"
|
CtxUnknownUserId = "_WG_SYS_UNKNOWN_"
|
||||||
|
CtxSystemLdapSyncer = "_WG_SYS_LDAP_SYNCER_"
|
||||||
|
CtxSystemWgImporter = "_WG_SYS_WG_IMPORTER_"
|
||||||
|
CtxSystemV1Migrator = "_WG_SYS_V1_MIGRATOR_"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContextUserInfo struct {
|
type ContextUserInfo struct {
|
||||||
@@ -28,6 +32,7 @@ func (u *ContextUserInfo) UserId() string {
|
|||||||
return string(u.Id)
|
return string(u.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultContextUserInfo returns a default context user info.
|
||||||
func DefaultContextUserInfo() *ContextUserInfo {
|
func DefaultContextUserInfo() *ContextUserInfo {
|
||||||
return &ContextUserInfo{
|
return &ContextUserInfo{
|
||||||
Id: CtxUnknownUserId,
|
Id: CtxUnknownUserId,
|
||||||
@@ -35,6 +40,7 @@ func DefaultContextUserInfo() *ContextUserInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SystemAdminContextUserInfo returns a context user info for the system admin.
|
||||||
func SystemAdminContextUserInfo() *ContextUserInfo {
|
func SystemAdminContextUserInfo() *ContextUserInfo {
|
||||||
return &ContextUserInfo{
|
return &ContextUserInfo{
|
||||||
Id: CtxSystemAdminId,
|
Id: CtxSystemAdminId,
|
||||||
@@ -42,6 +48,7 @@ func SystemAdminContextUserInfo() *ContextUserInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserInfoFromGin sets the user info from the gin context to the request context.
|
||||||
func SetUserInfoFromGin(c *gin.Context) context.Context {
|
func SetUserInfoFromGin(c *gin.Context) context.Context {
|
||||||
ginUserInfo, exists := c.Get(CtxUserInfo)
|
ginUserInfo, exists := c.Get(CtxUserInfo)
|
||||||
|
|
||||||
@@ -56,11 +63,13 @@ func SetUserInfoFromGin(c *gin.Context) context.Context {
|
|||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserInfo sets the user info in the context.
|
||||||
func SetUserInfo(ctx context.Context, info *ContextUserInfo) context.Context {
|
func SetUserInfo(ctx context.Context, info *ContextUserInfo) context.Context {
|
||||||
ctx = context.WithValue(ctx, CtxUserInfo, info)
|
ctx = context.WithValue(ctx, CtxUserInfo, info)
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserInfo returns the user info from the context.
|
||||||
func GetUserInfo(ctx context.Context) *ContextUserInfo {
|
func GetUserInfo(ctx context.Context) *ContextUserInfo {
|
||||||
rawInfo := ctx.Value(CtxUserInfo)
|
rawInfo := ctx.Value(CtxUserInfo)
|
||||||
if rawInfo == nil {
|
if rawInfo == nil {
|
||||||
@@ -74,6 +83,8 @@ func GetUserInfo(ctx context.Context) *ContextUserInfo {
|
|||||||
return DefaultContextUserInfo()
|
return DefaultContextUserInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateUserAccessRights checks if the current user has access rights to the requested user.
|
||||||
|
// If the user is an admin, access is granted.
|
||||||
func ValidateUserAccessRights(ctx context.Context, requiredUser UserIdentifier) error {
|
func ValidateUserAccessRights(ctx context.Context, requiredUser UserIdentifier) error {
|
||||||
sessionUser := GetUserInfo(ctx)
|
sessionUser := GetUserInfo(ctx)
|
||||||
|
|
||||||
@@ -86,9 +97,10 @@ func ValidateUserAccessRights(ctx context.Context, requiredUser UserIdentifier)
|
|||||||
}
|
}
|
||||||
|
|
||||||
logrus.Warnf("insufficient permissions for %s (want %s), stack: %s", sessionUser.Id, requiredUser, GetStackTrace())
|
logrus.Warnf("insufficient permissions for %s (want %s), stack: %s", sessionUser.Id, requiredUser, GetStackTrace())
|
||||||
return fmt.Errorf("insufficient permissions")
|
return ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateAdminAccessRights checks if the current user has admin access rights.
|
||||||
func ValidateAdminAccessRights(ctx context.Context) error {
|
func ValidateAdminAccessRights(ctx context.Context) error {
|
||||||
sessionUser := GetUserInfo(ctx)
|
sessionUser := GetUserInfo(ctx)
|
||||||
|
|
||||||
@@ -97,5 +109,5 @@ func ValidateAdminAccessRights(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logrus.Warnf("insufficient admin permissions for %s, stack: %s", sessionUser.Id, GetStackTrace())
|
logrus.Warnf("insufficient admin permissions for %s, stack: %s", sessionUser.Id, GetStackTrace())
|
||||||
return fmt.Errorf("insufficient permissions")
|
return ErrNoPermission
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,9 @@ import (
|
|||||||
|
|
||||||
var ErrNotFound = errors.New("record not found")
|
var ErrNotFound = errors.New("record not found")
|
||||||
var ErrNotUnique = errors.New("record not unique")
|
var ErrNotUnique = errors.New("record not unique")
|
||||||
|
var ErrNoPermission = errors.New("no permission")
|
||||||
|
var ErrDuplicateEntry = errors.New("duplicate entry")
|
||||||
|
var ErrInvalidData = errors.New("invalid data")
|
||||||
|
|
||||||
// GetStackTrace returns a stack trace of the current goroutine. The stack trace has at most 1024 bytes.
|
// GetStackTrace returns a stack trace of the current goroutine. The stack trace has at most 1024 bytes.
|
||||||
func GetStackTrace() string {
|
func GetStackTrace() string {
|
||||||
|
@@ -120,6 +120,13 @@ func (p *Peer) ApplyInterfaceDefaults(in *Interface) {
|
|||||||
p.Interface.PostDown.TrySetValue(in.PeerDefPostDown)
|
p.Interface.PostDown.TrySetValue(in.PeerDefPostDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Peer) GenerateDisplayName(prefix string) {
|
||||||
|
if prefix != "" {
|
||||||
|
prefix = fmt.Sprintf("%s ", strings.TrimSpace(prefix)) // add a space after the prefix
|
||||||
|
}
|
||||||
|
p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8))
|
||||||
|
}
|
||||||
|
|
||||||
type PeerInterfaceConfig struct {
|
type PeerInterfaceConfig struct {
|
||||||
KeyPair // private/public Key of the peer
|
KeyPair // private/public Key of the peer
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -42,6 +43,10 @@ type User struct {
|
|||||||
Locked *time.Time `gorm:"index;column:locked"` // if this field is set, the user is locked and can no longer login (WireGuard peers still can connect)
|
Locked *time.Time `gorm:"index;column:locked"` // if this field is set, the user is locked and can no longer login (WireGuard peers still can connect)
|
||||||
LockedReason string // the reason why the user has been locked
|
LockedReason string // the reason why the user has been locked
|
||||||
|
|
||||||
|
// API token for REST API access
|
||||||
|
ApiToken string `form:"api_token" binding:"omitempty"`
|
||||||
|
ApiTokenCreated *time.Time
|
||||||
|
|
||||||
LinkedPeerCount int `gorm:"-"`
|
LinkedPeerCount int `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +61,14 @@ func (u *User) IsLocked() bool {
|
|||||||
return u.Locked != nil
|
return u.Locked != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) IsApiEnabled() bool {
|
||||||
|
if u.ApiToken != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) CanChangePassword() error {
|
func (u *User) CanChangePassword() error {
|
||||||
if u.Source == UserSourceDatabase {
|
if u.Source == UserSourceDatabase {
|
||||||
return nil
|
return nil
|
||||||
@@ -115,6 +128,18 @@ func (u *User) CheckPassword(password string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) CheckApiToken(token string) error {
|
||||||
|
if !u.IsApiEnabled() {
|
||||||
|
return errors.New("api access disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if res := subtle.ConstantTimeCompare([]byte(u.ApiToken), []byte(token)); res != 1 {
|
||||||
|
return errors.New("wrong token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) HashPassword() error {
|
func (u *User) HashPassword() error {
|
||||||
if u.Password == "" {
|
if u.Password == "" {
|
||||||
return nil // nothing to hash
|
return nil // nothing to hash
|
||||||
|
@@ -79,6 +79,27 @@ func MapDefaultString(m map[string]interface{}, key string, dflt string) string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapDefaultStringSlice returns the string slice value for the given key or a default value
|
||||||
|
func MapDefaultStringSlice(m map[string]interface{}, key string, dflt []string) []string {
|
||||||
|
if m == nil {
|
||||||
|
return dflt
|
||||||
|
}
|
||||||
|
if tmp, ok := m[key]; !ok {
|
||||||
|
return dflt
|
||||||
|
} else {
|
||||||
|
switch v := tmp.(type) {
|
||||||
|
case []string:
|
||||||
|
return v
|
||||||
|
case string:
|
||||||
|
return []string{v}
|
||||||
|
case nil:
|
||||||
|
return dflt
|
||||||
|
default:
|
||||||
|
return []string{fmt.Sprintf("%v", v)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UniqueStringSlice removes duplicates in the given string slice
|
// UniqueStringSlice removes duplicates in the given string slice
|
||||||
func UniqueStringSlice(slice []string) []string {
|
func UniqueStringSlice(slice []string) []string {
|
||||||
keys := make(map[string]struct{})
|
keys := make(map[string]struct{})
|
||||||
|
@@ -4,7 +4,7 @@ site_description: Manage WireGuard Peers and Interface using a beautiful and sim
|
|||||||
site_url: https://wgportal.org/
|
site_url: https://wgportal.org/
|
||||||
repo_name: h44z/wg-portal
|
repo_name: h44z/wg-portal
|
||||||
repo_url: https://github.com/h44z/wg-portal
|
repo_url: https://github.com/h44z/wg-portal
|
||||||
copyright: Copyright © 2023-2024 WireGuard Portal Project
|
copyright: Copyright © 2023-2025 WireGuard Portal Project
|
||||||
|
|
||||||
extra_css:
|
extra_css:
|
||||||
- stylesheets/extra.css
|
- stylesheets/extra.css
|
||||||
@@ -21,12 +21,14 @@ theme:
|
|||||||
features:
|
features:
|
||||||
- navigation.instant
|
- navigation.instant
|
||||||
- navigation.tabs
|
- navigation.tabs
|
||||||
|
- navigation.expand
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- search
|
- search
|
||||||
- social
|
- social
|
||||||
- minify:
|
- minify:
|
||||||
minify_html: true
|
minify_html: true
|
||||||
|
- swagger-ui-tag
|
||||||
|
|
||||||
extra:
|
extra:
|
||||||
version:
|
version:
|
||||||
@@ -60,3 +62,7 @@ nav:
|
|||||||
- Building: documentation/getting-started/building.md
|
- Building: documentation/getting-started/building.md
|
||||||
- Docker Container: documentation/getting-started/docker.md
|
- Docker Container: documentation/getting-started/docker.md
|
||||||
- Upgrade from V1: documentation/getting-started/upgrade.md
|
- Upgrade from V1: documentation/getting-started/upgrade.md
|
||||||
|
- Configuration:
|
||||||
|
- Overview: documentation/configuration/overview.md
|
||||||
|
- Examples: documentation/configuration/examples.md
|
||||||
|
- REST API: documentation/rest-api/api-doc.md
|
||||||
|
Reference in New Issue
Block a user