From d596f578f68b1710b2bc01d2c8326184c6a8bfe1 Mon Sep 17 00:00:00 2001 From: h44z Date: Sat, 11 Jan 2025 18:44:55 +0100 Subject: [PATCH] API - CRUD for peers, interfaces and users (#340) Public REST API implementation to handle peers, interfaces and users. It also includes some simple provisioning endpoints. The Swagger API documentation is available under /api/v1/doc.html --- README.md | 185 +- cmd/api_build_tool/main.go | 2 +- cmd/wg-portal/main.go | 20 +- frontend/src/App.vue | 1 + frontend/src/components/UserViewModal.vue | 4 + frontend/src/helpers/models.js | 2 + frontend/src/lang/translations/de.json | 21 + frontend/src/lang/translations/en.json | 24 +- frontend/src/router/index.js | 8 + frontend/src/stores/profile.js | 28 + frontend/src/views/SettingsView.vue | 77 + internal/adapters/database.go | 24 + .../app/api/core/assets/doc/v0_swagger.json | 580 ++- .../app/api/core/assets/doc/v0_swagger.yaml | 397 +- .../app/api/core/assets/doc/v1_swagger.json | 1932 ++++++++ .../app/api/core/assets/doc/v1_swagger.yaml | 1358 ++++++ .../app/api/core/assets/js/rapidoc-min.js | 3917 ++++++++++++++++- .../app/api/core/assets/tpl/rapidoc.gohtml | 8 +- internal/app/api/core/server.go | 2 + internal/app/api/v0/handlers/base.go | 13 +- .../app/api/v0/handlers/endpoint_config.go | 11 +- .../app/api/v0/handlers/endpoint_peers.go | 38 +- .../app/api/v0/handlers/endpoint_users.go | 102 +- internal/app/api/v0/model/models.go | 1 + internal/app/api/v0/model/models_user.go | 49 +- .../app/api/v1/backend/interface_service.go | 109 + internal/app/api/v1/backend/peer_service.go | 143 + .../api/v1/backend/provisioning_service.go | 174 + internal/app/api/v1/backend/user_service.go | 107 + internal/app/api/v1/handlers/base.go | 81 + .../app/api/v1/handlers/endpoint_interface.go | 220 + internal/app/api/v1/handlers/endpoint_peer.go | 261 ++ .../api/v1/handlers/endpoint_provisioning.go | 195 + internal/app/api/v1/handlers/endpoint_user.go | 218 + .../v1/handlers/middleware_authentication.go | 92 + internal/app/api/v1/models/model_options.go | 46 + internal/app/api/v1/models/models.go | 8 + .../app/api/v1/models/models_interface.go | 201 + internal/app/api/v1/models/models_peer.go | 195 + .../app/api/v1/models/models_provisioning.go | 75 + internal/app/api/v1/models/models_user.go | 125 + internal/app/app.go | 14 +- internal/app/eventbus.go | 2 + internal/app/repos.go | 15 +- internal/app/users/repos.go | 2 + internal/app/users/user_manager.go | 127 +- .../app/wireguard/wireguard_interfaces.go | 9 +- internal/app/wireguard/wireguard_peers.go | 29 +- internal/config/config.go | 2 + internal/domain/context.go | 13 +- internal/domain/errors.go | 3 + internal/domain/peer.go | 7 + internal/domain/user.go | 25 + 53 files changed, 11028 insertions(+), 274 deletions(-) create mode 100644 frontend/src/views/SettingsView.vue create mode 100644 internal/app/api/core/assets/doc/v1_swagger.json create mode 100644 internal/app/api/core/assets/doc/v1_swagger.yaml create mode 100644 internal/app/api/v1/backend/interface_service.go create mode 100644 internal/app/api/v1/backend/peer_service.go create mode 100644 internal/app/api/v1/backend/provisioning_service.go create mode 100644 internal/app/api/v1/backend/user_service.go create mode 100644 internal/app/api/v1/handlers/base.go create mode 100644 internal/app/api/v1/handlers/endpoint_interface.go create mode 100644 internal/app/api/v1/handlers/endpoint_peer.go create mode 100644 internal/app/api/v1/handlers/endpoint_provisioning.go create mode 100644 internal/app/api/v1/handlers/endpoint_user.go create mode 100644 internal/app/api/v1/handlers/middleware_authentication.go create mode 100644 internal/app/api/v1/models/model_options.go create mode 100644 internal/app/api/v1/models/models.go create mode 100644 internal/app/api/v1/models/models_interface.go create mode 100644 internal/app/api/v1/models/models_peer.go create mode 100644 internal/app/api/v1/models/models_provisioning.go create mode 100644 internal/app/api/v1/models/models_user.go diff --git a/README.md b/README.md index 3d086dd..6c53eaf 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL or Post * Peer Expiry Feature * Handle route and DNS settings like wg-quick does * Exposes Prometheus [metrics](#metrics) - * ~~REST API for management and client deployment~~ (coming soon) + * REST API for management and client deployment ![Screenshot](screenshot.png) @@ -55,97 +55,98 @@ By default, WireGuard Portal uses a SQLite database. The database is stored in * ### Configuration Options The following configuration options are available: -| configuration key | parent key | default_value | description | -|----------------------------------|------------|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| -| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. | -| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. | -| editable_keys | core | true | Allow to edit key-pairs in the UI. | -| create_default_peer | core | false | If an LDAP user logs in for the first time and has no peers associated, a new WireGuard peer will be created for all server interfaces. | -| create_default_peer_on_creation | core | false | If an LDAP user is created (e.g. through LDAP sync), a new WireGuard peer will be created for all server interfaces. | -| re_enable_peer_after_user_enable | core | true | Re-enable all peers that were previously disabled due to a user disable action. | -| delete_peer_after_user_deleted | core | false | Delete all linked peers if a user gets disabled. Otherwise the peers only get disabled. | -| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. | -| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. | -| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. | -| log_level | advanced | info | The loglevel, can be one of: trace, debug, info, warn, error. | -| log_pretty | advanced | false | Uses pretty, colorized log messages. | -| log_json | advanced | false | Logs in JSON format. | -| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. | -| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. | -| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. | -| use_ip_v6 | advanced | true | Enable IPv6 support. | -| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. | -| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. | -| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. | -| route_table_offset | advanced | 20000 | The default offset for ip route table id's. | -| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. | -| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. | -| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). | -| ping_check_interval | statistics | 1m | The interval time between two ping check runs. | -| data_collection_interval | statistics | 1m | The interval between the data collection cycles. | -| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. | -| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. | -| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. | -| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. | -| host | mail | 127.0.0.1 | The mail-server address. | -| port | mail | 25 | The mail-server SMTP port. | -| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. | -| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). | -| username | mail | | The SMTP user name. | -| password | mail | | The SMTP password. | -| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. | -| from | mail | Wireguard Portal | The address that is used to send mails. | -| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. | -| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. | -| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. | -| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. | -| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). | -| display_name | auth/oidc | | The display name is shown at the login page (the login button). | -| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". | -| client_id | auth/oidc | | The OAuth client id. | -| client_secret | auth/oidc | | The OAuth client secret. | -| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. | -| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. | -| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. | -| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). | -| display_name | auth/oauth | | The display name is shown at the login page (the login button). | -| client_id | auth/oauth | | The OAuth client id. | -| client_secret | auth/oauth | | The OAuth client secret. | -| auth_url | auth/oauth | | The URL for the authentication endpoint. | -| token_url | auth/oauth | | The URL for the token endpoint. | -| user_info_url | auth/oauth | | The URL for the user information endpoint. | -| scopes | auth/oauth | | OAuth scopes. | -| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. | -| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. | -| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 | -| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. | -| cert_validation | auth/ldap | | Validate the LDAP server certificate. | -| tls_certificate_path | auth/ldap | | A path to the TLS certificate. | -| tls_key_path | auth/ldap | | A path to the TLS key. | -| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL | -| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard | -| bind_pass | auth/ldap | | The bind password. | -| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. | -| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. | -| admin_group | auth/ldap | | Users in this group are marked as administrators. | -| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. | -| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. | -| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. | -| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. | -| debug | database | false | Debug database statements (log each statement). | -| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. | -| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. | -| dsn | database | data/sqlite.db | The database DSN. For example: user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local | -| request_logging | web | false | Log all HTTP requests. | -| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. | -| listening_address | web | :8888 | The listening port of the web server. | -| session_identifier | web | wgPortalSession | The session identifier for the web frontend. | -| session_secret | web | very_secret | The session secret for the web frontend. | -| csrf_secret | web | extremely_secret | The CSRF secret. | -| site_title | web | WireGuard Portal | The title that is shown in the web frontend. | -| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. | -| cert_file | web | | (Optional) Path to the TLS certificate file | -| key_file | web | | (Optional) Path to the TLS certificate key file | +| configuration key | parent key | default_value | description | +|----------------------------------|------------|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| +| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. | +| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. | +| editable_keys | core | true | Allow to edit key-pairs in the UI. | +| create_default_peer | core | false | If an LDAP user logs in for the first time and has no peers associated, a new WireGuard peer will be created for all server interfaces. | +| create_default_peer_on_creation | core | false | If an LDAP user is created (e.g. through LDAP sync), a new WireGuard peer will be created for all server interfaces. | +| re_enable_peer_after_user_enable | core | true | Re-enable all peers that were previously disabled due to a user disable action. | +| delete_peer_after_user_deleted | core | false | Delete all linked peers if a user gets disabled. Otherwise the peers only get disabled. | +| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. | +| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. | +| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. | +| log_level | advanced | info | The loglevel, can be one of: trace, debug, info, warn, error. | +| log_pretty | advanced | false | Uses pretty, colorized log messages. | +| log_json | advanced | false | Logs in JSON format. | +| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. | +| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. | +| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. | +| use_ip_v6 | advanced | true | Enable IPv6 support. | +| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. | +| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. | +| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. | +| route_table_offset | advanced | 20000 | The default offset for ip route table id's. | +| api_admin_only | advanced | true | This flag specifies if the public REST API is available to administrators only. The API Swagger documentation is available under /api/v1/doc.html | +| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. | +| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. | +| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). | +| ping_check_interval | statistics | 1m | The interval time between two ping check runs. | +| data_collection_interval | statistics | 1m | The interval between the data collection cycles. | +| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. | +| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. | +| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. | +| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. | +| host | mail | 127.0.0.1 | The mail-server address. | +| port | mail | 25 | The mail-server SMTP port. | +| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. | +| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). | +| username | mail | | The SMTP user name. | +| password | mail | | The SMTP password. | +| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. | +| from | mail | Wireguard Portal | The address that is used to send mails. | +| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. | +| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. | +| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. | +| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. | +| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). | +| display_name | auth/oidc | | The display name is shown at the login page (the login button). | +| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". | +| client_id | auth/oidc | | The OAuth client id. | +| client_secret | auth/oidc | | The OAuth client secret. | +| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. | +| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. | +| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. | +| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). | +| display_name | auth/oauth | | The display name is shown at the login page (the login button). | +| client_id | auth/oauth | | The OAuth client id. | +| client_secret | auth/oauth | | The OAuth client secret. | +| auth_url | auth/oauth | | The URL for the authentication endpoint. | +| token_url | auth/oauth | | The URL for the token endpoint. | +| user_info_url | auth/oauth | | The URL for the user information endpoint. | +| scopes | auth/oauth | | OAuth scopes. | +| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. | +| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. | +| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 | +| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. | +| cert_validation | auth/ldap | | Validate the LDAP server certificate. | +| tls_certificate_path | auth/ldap | | A path to the TLS certificate. | +| tls_key_path | auth/ldap | | A path to the TLS key. | +| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL | +| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard | +| bind_pass | auth/ldap | | The bind password. | +| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. | +| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. | +| admin_group | auth/ldap | | Users in this group are marked as administrators. | +| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. | +| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. | +| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. | +| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. | +| debug | database | false | Debug database statements (log each statement). | +| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. | +| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. | +| dsn | database | data/sqlite.db | The database DSN. For example: user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local | +| request_logging | web | false | Log all HTTP requests. | +| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. | +| listening_address | web | :8888 | The listening port of the web server. | +| session_identifier | web | wgPortalSession | The session identifier for the web frontend. | +| session_secret | web | very_secret | The session secret for the web frontend. | +| csrf_secret | web | extremely_secret | The CSRF secret. | +| site_title | web | WireGuard Portal | The title that is shown in the web frontend. | +| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. | +| cert_file | web | | (Optional) Path to the TLS certificate file | +| key_file | web | | (Optional) Path to the TLS certificate key file | ## Upgrading from V1 diff --git a/cmd/api_build_tool/main.go b/cmd/api_build_tool/main.go index 50dd3e4..9bad0b1 100644 --- a/cmd/api_build_tool/main.go +++ b/cmd/api_build_tool/main.go @@ -20,7 +20,7 @@ func main() { } apiBasePath := filepath.Join(wd, "/internal/app/api") - apis := []string{"v0"} + apis := []string{"v0", "v1"} hasError := false for _, apiVersion := range apis { diff --git a/cmd/wg-portal/main.go b/cmd/wg-portal/main.go index 948e702..da2876a 100644 --- a/cmd/wg-portal/main.go +++ b/cmd/wg-portal/main.go @@ -9,6 +9,8 @@ import ( "github.com/h44z/wg-portal/internal/app/api/core" handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers" + backendV1 "github.com/h44z/wg-portal/internal/app/api/v1/backend" + handlersV1 "github.com/h44z/wg-portal/internal/app/api/v1/handlers" "github.com/h44z/wg-portal/internal/app/audit" "github.com/h44z/wg-portal/internal/app/auth" "github.com/h44z/wg-portal/internal/app/configfile" @@ -103,7 +105,23 @@ func main() { apiFrontend := handlersV0.NewRestApi(cfg, backend) - webSrv, err := core.NewServer(cfg, apiFrontend) + apiV1BackendUsers := backendV1.NewUserService(cfg, userManager) + apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager) + apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager) + apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager) + apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1BackendUsers) + apiV1EndpointPeers := handlersV1.NewPeerEndpoint(apiV1BackendPeers) + apiV1EndpointInterfaces := handlersV1.NewInterfaceEndpoint(apiV1BackendInterfaces) + apiV1EndpointProvisioning := handlersV1.NewProvisioningEndpoint(apiV1BackendProvisioning) + apiV1 := handlersV1.NewRestApi( + userManager, + apiV1EndpointUsers, + apiV1EndpointPeers, + apiV1EndpointInterfaces, + apiV1EndpointProvisioning, + ) + + webSrv, err := core.NewServer(cfg, apiFrontend, apiV1) internal.AssertNoError(err) go metricsServer.Run(ctx) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index e2b4f29..b0dd782 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -90,6 +90,7 @@ const currentYear = ref(new Date().getFullYear()) href="#" role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }} diff --git a/frontend/src/components/UserViewModal.vue b/frontend/src/components/UserViewModal.vue index c2edbb7..cb844e0 100644 --- a/frontend/src/components/UserViewModal.vue +++ b/frontend/src/components/UserViewModal.vue @@ -88,6 +88,10 @@ function close() { {{ $t('modals.user-view.department') }}: {{selectedUser.Department}} + + {{ $t('modals.user-view.api-enabled') }}: + {{selectedUser.ApiEnabled}} + {{ $t('modals.user-view.disabled') }}: {{selectedUser.DisabledReason}} diff --git a/frontend/src/helpers/models.js b/frontend/src/helpers/models.js index 3dfa4d6..0c33315 100644 --- a/frontend/src/helpers/models.js +++ b/frontend/src/helpers/models.js @@ -146,6 +146,8 @@ export function freshUser() { Locked: false, LockedReason: "", + ApiEnabled: false, + PeerCount: 0 } } diff --git a/frontend/src/lang/translations/de.json b/frontend/src/lang/translations/de.json index a498111..9f0cab3 100644 --- a/frontend/src/lang/translations/de.json +++ b/frontend/src/lang/translations/de.json @@ -37,6 +37,7 @@ "users": "Benutzer", "lang": "Sprache ändern", "profile": "Mein Profil", + "settings": "Einstellungen", "login": "Anmelden", "logout": "Abmelden" }, @@ -167,6 +168,26 @@ "button-show-peer": "Show Peer", "button-edit-peer": "Edit Peer" }, + "settings": { + "headline": "Einstellungen", + "abstract": "Hier finden Sie persönliche Einstellungen für WireGuard Portal.", + "api": { + "headline": "API Einstellungen", + "abstract": "Hier können Sie die RESTful API verwalten.", + "active-description": "Die API ist derzeit für Ihr Benutzerkonto aktiv. Alle API-Anfragen werden mit Basic Auth authentifiziert. Verwenden Sie zur Authentifizierung die folgenden Anmeldeinformationen.", + "inactive-description": "Die API ist derzeit inaktiv. Klicken Sie auf die Schaltfläche unten, um sie zu aktivieren.", + "user-label": "API Benutzername:", + "user-placeholder": "API Benutzer", + "token-label": "API Passwort:", + "token-placeholder": "API Token", + "token-created-label": "API-Zugriff gewährt seit: ", + "button-disable-title": "Deaktivieren Sie die API. Dadurch wird der aktuelle Token ungültig.", + "button-disable-text": "API deaktivieren", + "button-enable-title": "Aktivieren Sie die API, dadurch wird ein neuer Token generiert.", + "button-enable-text": "API aktivieren", + "api-link": "API Dokumentation" + } + }, "modals": { "user-view": { "headline": "User Account:", diff --git a/frontend/src/lang/translations/en.json b/frontend/src/lang/translations/en.json index 08ad45f..19842e8 100644 --- a/frontend/src/lang/translations/en.json +++ b/frontend/src/lang/translations/en.json @@ -37,6 +37,7 @@ "users": "Users", "lang": "Toggle Language", "profile": "My Profile", + "settings": "Settings", "login": "Login", "logout": "Logout" }, @@ -167,6 +168,26 @@ "button-show-peer": "Show Peer", "button-edit-peer": "Edit Peer" }, + "settings": { + "headline": "Settings", + "abstract": "Here you can change your personal settings.", + "api": { + "headline": "API Settings", + "abstract": "Here you can configure the RESTful API settings.", + "active-description": "The API is currently active for your user account. All API requests are authenticated with Basic Auth. Use the following credentials for authentication.", + "inactive-description": "The API is currently inactive. Press the button below to activate it.", + "user-label": "API Username:", + "user-placeholder": "The API user", + "token-label": "API Password:", + "token-placeholder": "The API token", + "token-created-label": "API access granted at: ", + "button-disable-title": "Disable API, this will invalidate the current token.", + "button-disable-text": "Disable API", + "button-enable-title": "Enable API, this will generate a new token.", + "button-enable-text": "Enable API", + "api-link": "API Documentation" + } + }, "modals": { "user-view": { "headline": "User Account:", @@ -177,8 +198,9 @@ "email": "E-Mail", "firstname": "Firstname", "lastname": "Lastname", - "phone": "Phone number", + "phone": "Phone Number", "department": "Department", + "api-enabled": "API Access", "disabled": "Account Disabled", "locked": "Account Locked", "no-peers": "User has no associated peers.", diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 09ef969..25adf58 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -47,6 +47,14 @@ const router = createRouter({ // this generates a separate chunk (About.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import('../views/ProfileView.vue') + }, + { + path: '/settings', + name: 'settings', + // route level code-splitting + // this generates a separate chunk (About.[hash].js) for this route + // which is lazy-loaded when the route is visited. + component: () => import('../views/SettingsView.vue') } ], linkActiveClass: "active", diff --git a/frontend/src/stores/profile.js b/frontend/src/stores/profile.js index 1f16f56..fcf5856 100644 --- a/frontend/src/stores/profile.js +++ b/frontend/src/stores/profile.js @@ -116,6 +116,34 @@ export const profileStore = defineStore({ this.stats = statsResponse.Stats 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() { this.fetching = true let currentUser = authStore().user.Identifier diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue new file mode 100644 index 0000000..ef2f73c --- /dev/null +++ b/frontend/src/views/SettingsView.vue @@ -0,0 +1,77 @@ + + + diff --git a/internal/adapters/database.go b/internal/adapters/database.go index f4a778c..98e4967 100644 --- a/internal/adapters/database.go +++ b/internal/adapters/database.go @@ -698,6 +698,30 @@ func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domai return &user, nil } +func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) { + var users []domain.User + + err := r.db.WithContext(ctx).Where("email = ?", email).Find(&users).Error + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + return nil, domain.ErrNotFound + } + if err != nil { + return nil, err + } + + if len(users) == 0 { + return nil, domain.ErrNotFound + } + + if len(users) > 1 { + return nil, fmt.Errorf("found multiple users with email %s: %w", email, domain.ErrNotUnique) + } + + user := users[0] + + return &user, nil +} + func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) { var users []domain.User diff --git a/internal/app/api/core/assets/doc/v0_swagger.json b/internal/app/api/core/assets/doc/v0_swagger.json index ee1ed7a..b37f2cf 100644 --- a/internal/app/api/core/assets/doc/v0_swagger.json +++ b/internal/app/api/core/assets/doc/v0_swagger.json @@ -1,8 +1,8 @@ { "swagger": "2.0", "info": { - "description": "WireGuard Portal API - a testing API endpoint", - "title": "WireGuard Portal API", + "description": "WireGuard Portal API - UI Endpoints", + "title": "WireGuard Portal SPA-UI API", "contact": { "name": "WireGuard Portal Developers", "url": "https://github.com/h44z/wg-portal" @@ -175,6 +175,26 @@ } } }, + "/config/settings": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Configuration" + ], + "summary": "Get the frontend settings object.", + "operationId": "config_handleSettingsGet", + "responses": { + "200": { + "description": "The JavaScript contents", + "schema": { + "type": "string" + } + } + } + } + }, "/csrf": { "get": { "produces": [ @@ -499,6 +519,91 @@ } } }, + "/interface/{id}/apply-peer-defaults": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Interface" + ], + "summary": "Apply all peer defaults to the available peers.", + "operationId": "interfaces_handleApplyPeerDefaultsPost", + "parameters": [ + { + "type": "string", + "description": "The interface identifier", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "The interface data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Interface" + } + } + ], + "responses": { + "204": { + "description": "No content if applying peer defaults was successful" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.Error" + } + } + } + } + }, + "/interface/{id}/save-config": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Interface" + ], + "summary": "Save the interface configuration in wg-quick format to a file.", + "operationId": "interfaces_handleSaveConfigPost", + "parameters": [ + { + "type": "string", + "description": "The interface identifier", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content if saving the configuration was successful" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.Error" + } + } + } + } + }, "/now": { "get": { "description": "Nothing more to describe...", @@ -526,9 +631,50 @@ } } }, + "/peer/config-mail": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Peer" + ], + "summary": "Send peer configuration via email.", + "operationId": "peers_handleEmailPost", + "parameters": [ + { + "description": "The peer mail request data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.PeerMailRequest" + } + } + ], + "responses": { + "204": { + "description": "No content if mail sending was successful" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.Error" + } + } + } + } + }, "/peer/config-qr/{id}": { "get": { "produces": [ + "image/png", "application/json" ], "tags": [ @@ -536,11 +682,20 @@ ], "summary": "Get peer configuration as qr code.", "operationId": "peers_handleQrCodeGet", + "parameters": [ + { + "type": "string", + "description": "The peer identifier", + "name": "id", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK", "schema": { - "type": "string" + "type": "file" } }, "400": { @@ -568,6 +723,15 @@ ], "summary": "Get peer configuration as string.", "operationId": "peers_handleConfigGet", + "parameters": [ + { + "type": "string", + "description": "The peer identifier", + "name": "id", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK", @@ -634,6 +798,59 @@ } } }, + "/peer/iface/{iface}/multiplenew": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Peer" + ], + "summary": "Create multiple new peers for the given interface.", + "operationId": "peers_handleCreateMultiplePost", + "parameters": [ + { + "type": "string", + "description": "The interface identifier", + "name": "iface", + "in": "path", + "required": true + }, + { + "description": "The peer creation request data", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.MultiPeerRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Peer" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.Error" + } + } + } + } + }, "/peer/iface/{iface}/new": { "post": { "produces": [ @@ -725,6 +942,47 @@ } } }, + "/peer/iface/{iface}/stats": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Peer" + ], + "summary": "Get peer stats for the given interface.", + "operationId": "peers_handleStatsGet", + "parameters": [ + { + "type": "string", + "description": "The interface identifier", + "name": "iface", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.PeerStats" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.Error" + } + } + } + } + }, "/peer/{id}": { "get": { "produces": [ @@ -1041,6 +1299,70 @@ } } }, + "/user/{id}/api/disable": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Disable the REST API for the given user.", + "operationId": "users_handleApiDisablePost", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.Error" + } + } + } + } + }, + "/user/{id}/api/enable": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Enable the REST API for the given user.", + "operationId": "users_handleApiEnablePost", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.Error" + } + } + } + } + }, "/user/{id}/peers": { "get": { "produces": [ @@ -1061,6 +1383,44 @@ } } }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.Error" + } + } + } + } + }, + "/user/{id}/stats": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get peer stats for the given user.", + "operationId": "users_handleStatsGet", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.PeerStats" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.Error" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -1072,6 +1432,53 @@ } }, "definitions": { + "model.ConfigOption-array_string": { + "type": "object", + "properties": { + "Overridable": { + "type": "boolean" + }, + "Value": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "model.ConfigOption-int": { + "type": "object", + "properties": { + "Overridable": { + "type": "boolean" + }, + "Value": { + "type": "integer" + } + } + }, + "model.ConfigOption-string": { + "type": "object", + "properties": { + "Overridable": { + "type": "boolean" + }, + "Value": { + "type": "string" + } + } + }, + "model.ConfigOption-uint32": { + "type": "object", + "properties": { + "Overridable": { + "type": "boolean" + }, + "Value": { + "type": "integer" + } + } + }, "model.Error": { "type": "object", "properties": { @@ -1083,25 +1490,11 @@ } } }, - "model.Int32ConfigOption": { + "model.ExpiryDate": { "type": "object", "properties": { - "Overridable": { - "type": "boolean" - }, - "Value": { - "type": "integer" - } - } - }, - "model.IntConfigOption": { - "type": "object", - "properties": { - "Overridable": { - "type": "boolean" - }, - "Value": { - "type": "integer" + "time.Time": { + "type": "string" } } }, @@ -1290,6 +1683,20 @@ } } }, + "model.MultiPeerRequest": { + "type": "object", + "properties": { + "Identifiers": { + "type": "array", + "items": { + "type": "string" + } + }, + "Suffix": { + "type": "string" + } + } + }, "model.Peer": { "type": "object", "properties": { @@ -1304,7 +1711,7 @@ "description": "all allowed ip subnets, comma seperated", "allOf": [ { - "$ref": "#/definitions/model.StringSliceConfigOption" + "$ref": "#/definitions/model.ConfigOption-array_string" } ] }, @@ -1328,7 +1735,7 @@ "description": "the dns server that should be set if the interface is up, comma separated", "allOf": [ { - "$ref": "#/definitions/model.StringSliceConfigOption" + "$ref": "#/definitions/model.ConfigOption-array_string" } ] }, @@ -1336,7 +1743,7 @@ "description": "the dns search option string that should be set if the interface is up, will be appended to DnsStr", "allOf": [ { - "$ref": "#/definitions/model.StringSliceConfigOption" + "$ref": "#/definitions/model.ConfigOption-array_string" } ] }, @@ -1344,7 +1751,7 @@ "description": "the endpoint address", "allOf": [ { - "$ref": "#/definitions/model.StringConfigOption" + "$ref": "#/definitions/model.ConfigOption-string" } ] }, @@ -1352,13 +1759,17 @@ "description": "the endpoint public key", "allOf": [ { - "$ref": "#/definitions/model.StringConfigOption" + "$ref": "#/definitions/model.ConfigOption-string" } ] }, "ExpiresAt": { "description": "expiry dates for peers", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/model.ExpiryDate" + } + ] }, "ExtraAllowedIPs": { "description": "all allowed ip subnets on the server side, comma seperated", @@ -1371,7 +1782,7 @@ "description": "a firewall mark", "allOf": [ { - "$ref": "#/definitions/model.Int32ConfigOption" + "$ref": "#/definitions/model.ConfigOption-uint32" } ] }, @@ -1392,7 +1803,7 @@ "description": "the device MTU", "allOf": [ { - "$ref": "#/definitions/model.IntConfigOption" + "$ref": "#/definitions/model.ConfigOption-int" } ] }, @@ -1404,7 +1815,7 @@ "description": "the persistent keep-alive interval", "allOf": [ { - "$ref": "#/definitions/model.IntConfigOption" + "$ref": "#/definitions/model.ConfigOption-int" } ] }, @@ -1412,7 +1823,7 @@ "description": "action that is executed after the device is down", "allOf": [ { - "$ref": "#/definitions/model.StringConfigOption" + "$ref": "#/definitions/model.ConfigOption-string" } ] }, @@ -1420,7 +1831,7 @@ "description": "action that is executed after the device is up", "allOf": [ { - "$ref": "#/definitions/model.StringConfigOption" + "$ref": "#/definitions/model.ConfigOption-string" } ] }, @@ -1428,7 +1839,7 @@ "description": "action that is executed before the device is down", "allOf": [ { - "$ref": "#/definitions/model.StringConfigOption" + "$ref": "#/definitions/model.ConfigOption-string" } ] }, @@ -1436,7 +1847,7 @@ "description": "action that is executed before the device is up", "allOf": [ { - "$ref": "#/definitions/model.StringConfigOption" + "$ref": "#/definitions/model.ConfigOption-string" } ] }, @@ -1458,7 +1869,7 @@ "description": "the routing table", "allOf": [ { - "$ref": "#/definitions/model.StringConfigOption" + "$ref": "#/definitions/model.ConfigOption-string" } ] }, @@ -1468,6 +1879,66 @@ } } }, + "model.PeerMailRequest": { + "type": "object", + "properties": { + "Identifiers": { + "type": "array", + "items": { + "type": "string" + } + }, + "LinkOnly": { + "type": "boolean" + } + } + }, + "model.PeerStatData": { + "type": "object", + "properties": { + "BytesReceived": { + "type": "integer" + }, + "BytesTransmitted": { + "type": "integer" + }, + "EndpointAddress": { + "type": "string" + }, + "IsConnected": { + "type": "boolean" + }, + "IsPingable": { + "type": "boolean" + }, + "LastHandshake": { + "type": "string" + }, + "LastPing": { + "type": "string" + }, + "LastSessionStart": { + "type": "string" + } + } + }, + "model.PeerStats": { + "type": "object", + "properties": { + "Enabled": { + "description": "peer stats tracking enabled", + "type": "boolean", + "example": true + }, + "Stats": { + "description": "stats, map key = Peer identifier", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/model.PeerStatData" + } + } + } + }, "model.SessionInfo": { "type": "object", "properties": { @@ -1491,34 +1962,35 @@ } } }, - "model.StringConfigOption": { + "model.Settings": { "type": "object", "properties": { - "Overridable": { + "ApiAdminOnly": { "type": "boolean" }, - "Value": { - "type": "string" - } - } - }, - "model.StringSliceConfigOption": { - "type": "object", - "properties": { - "Overridable": { + "MailLinkOnly": { "type": "boolean" }, - "Value": { - "type": "array", - "items": { - "type": "string" - } + "PersistentConfigSupported": { + "type": "boolean" + }, + "SelfProvisioning": { + "type": "boolean" } } }, "model.User": { "type": "object", "properties": { + "ApiEnabled": { + "type": "boolean" + }, + "ApiToken": { + "type": "string" + }, + "ApiTokenCreated": { + "type": "string" + }, "Department": { "type": "string" }, @@ -1545,6 +2017,14 @@ "Lastname": { "type": "string" }, + "Locked": { + "description": "if this field is set, the user is locked", + "type": "boolean" + }, + "LockedReason": { + "description": "the reason why the user has been locked", + "type": "string" + }, "Notes": { "type": "string" }, diff --git a/internal/app/api/core/assets/doc/v0_swagger.yaml b/internal/app/api/core/assets/doc/v0_swagger.yaml index 5e921db..ee870dc 100644 --- a/internal/app/api/core/assets/doc/v0_swagger.yaml +++ b/internal/app/api/core/assets/doc/v0_swagger.yaml @@ -1,5 +1,35 @@ basePath: /api/v0 definitions: + model.ConfigOption-array_string: + properties: + Overridable: + type: boolean + Value: + items: + type: string + type: array + type: object + model.ConfigOption-int: + properties: + Overridable: + type: boolean + Value: + type: integer + type: object + model.ConfigOption-string: + properties: + Overridable: + type: boolean + Value: + type: string + type: object + model.ConfigOption-uint32: + properties: + Overridable: + type: boolean + Value: + type: integer + type: object model.Error: properties: Code: @@ -7,19 +37,10 @@ definitions: Message: type: string type: object - model.Int32ConfigOption: + model.ExpiryDate: properties: - Overridable: - type: boolean - Value: - type: integer - type: object - model.IntConfigOption: - properties: - Overridable: - type: boolean - Value: - type: integer + time.Time: + type: string type: object model.Interface: properties: @@ -160,6 +181,15 @@ definitions: example: /auth/google/login type: string type: object + model.MultiPeerRequest: + properties: + Identifiers: + items: + type: string + type: array + Suffix: + type: string + type: object model.Peer: properties: Addresses: @@ -169,7 +199,7 @@ definitions: type: array AllowedIPs: allOf: - - $ref: '#/definitions/model.StringSliceConfigOption' + - $ref: '#/definitions/model.ConfigOption-array_string' description: all allowed ip subnets, comma seperated CheckAliveAddress: description: optional ip address or DNS name that is used for ping checks @@ -185,25 +215,26 @@ definitions: type: string Dns: allOf: - - $ref: '#/definitions/model.StringSliceConfigOption' + - $ref: '#/definitions/model.ConfigOption-array_string' description: the dns server that should be set if the interface is up, comma separated DnsSearch: allOf: - - $ref: '#/definitions/model.StringSliceConfigOption' + - $ref: '#/definitions/model.ConfigOption-array_string' description: the dns search option string that should be set if the interface is up, will be appended to DnsStr Endpoint: allOf: - - $ref: '#/definitions/model.StringConfigOption' + - $ref: '#/definitions/model.ConfigOption-string' description: the endpoint address EndpointPublicKey: allOf: - - $ref: '#/definitions/model.StringConfigOption' + - $ref: '#/definitions/model.ConfigOption-string' description: the endpoint public key ExpiresAt: + allOf: + - $ref: '#/definitions/model.ExpiryDate' description: expiry dates for peers - type: string ExtraAllowedIPs: description: all allowed ip subnets on the server side, comma seperated items: @@ -211,7 +242,7 @@ definitions: type: array FirewallMark: allOf: - - $ref: '#/definitions/model.Int32ConfigOption' + - $ref: '#/definitions/model.ConfigOption-uint32' description: a firewall mark Identifier: description: peer unique identifier @@ -225,30 +256,30 @@ definitions: type: string Mtu: allOf: - - $ref: '#/definitions/model.IntConfigOption' + - $ref: '#/definitions/model.ConfigOption-int' description: the device MTU Notes: description: a note field for peers type: string PersistentKeepalive: allOf: - - $ref: '#/definitions/model.IntConfigOption' + - $ref: '#/definitions/model.ConfigOption-int' description: the persistent keep-alive interval PostDown: allOf: - - $ref: '#/definitions/model.StringConfigOption' + - $ref: '#/definitions/model.ConfigOption-string' description: action that is executed after the device is down PostUp: allOf: - - $ref: '#/definitions/model.StringConfigOption' + - $ref: '#/definitions/model.ConfigOption-string' description: action that is executed after the device is up PreDown: allOf: - - $ref: '#/definitions/model.StringConfigOption' + - $ref: '#/definitions/model.ConfigOption-string' description: action that is executed before the device is down PreUp: allOf: - - $ref: '#/definitions/model.StringConfigOption' + - $ref: '#/definitions/model.ConfigOption-string' description: action that is executed before the device is up PresharedKey: description: the pre-shared Key of the peer @@ -263,12 +294,52 @@ definitions: type: string RoutingTable: allOf: - - $ref: '#/definitions/model.StringConfigOption' + - $ref: '#/definitions/model.ConfigOption-string' description: the routing table UserIdentifier: description: the owner type: string type: object + model.PeerMailRequest: + properties: + Identifiers: + items: + type: string + type: array + LinkOnly: + type: boolean + type: object + model.PeerStatData: + properties: + BytesReceived: + type: integer + BytesTransmitted: + type: integer + EndpointAddress: + type: string + IsConnected: + type: boolean + IsPingable: + type: boolean + LastHandshake: + type: string + LastPing: + type: string + LastSessionStart: + type: string + type: object + model.PeerStats: + properties: + Enabled: + description: peer stats tracking enabled + example: true + type: boolean + Stats: + additionalProperties: + $ref: '#/definitions/model.PeerStatData' + description: stats, map key = Peer identifier + type: object + type: object model.SessionInfo: properties: IsAdmin: @@ -284,24 +355,25 @@ definitions: UserLastname: type: string type: object - model.StringConfigOption: + model.Settings: properties: - Overridable: + ApiAdminOnly: type: boolean - Value: - type: string - type: object - model.StringSliceConfigOption: - properties: - Overridable: + MailLinkOnly: + type: boolean + PersistentConfigSupported: + type: boolean + SelfProvisioning: type: boolean - Value: - items: - type: string - type: array type: object model.User: properties: + ApiEnabled: + type: boolean + ApiToken: + type: string + ApiTokenCreated: + type: string Department: type: string Disabled: @@ -320,6 +392,12 @@ definitions: type: boolean Lastname: type: string + Locked: + description: if this field is set, the user is locked + type: boolean + LockedReason: + description: the reason why the user has been locked + type: string Notes: type: string Password: @@ -337,8 +415,8 @@ info: contact: name: WireGuard Portal Developers url: https://github.com/h44z/wg-portal - description: WireGuard Portal API - a testing API endpoint - title: WireGuard Portal API + description: WireGuard Portal API - UI Endpoints + title: WireGuard Portal SPA-UI API version: "0.0" paths: /auth/{provider}/callback: @@ -448,6 +526,19 @@ paths: summary: Get the dynamic frontend configuration javascript. tags: - Configuration + /config/settings: + get: + operationId: config_handleSettingsGet + produces: + - application/json + responses: + "200": + description: The JavaScript contents + schema: + type: string + summary: Get the frontend settings object. + tags: + - Configuration /csrf: get: operationId: base_handleCsrfGet @@ -536,6 +627,62 @@ paths: summary: Update the interface record. tags: - Interface + /interface/{id}/apply-peer-defaults: + post: + operationId: interfaces_handleApplyPeerDefaultsPost + parameters: + - description: The interface identifier + in: path + name: id + required: true + type: string + - description: The interface data + in: body + name: request + required: true + schema: + $ref: '#/definitions/model.Interface' + produces: + - application/json + responses: + "204": + description: No content if applying peer defaults was successful + "400": + description: Bad Request + schema: + $ref: '#/definitions/model.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/model.Error' + summary: Apply all peer defaults to the available peers. + tags: + - Interface + /interface/{id}/save-config: + post: + operationId: interfaces_handleSaveConfigPost + parameters: + - description: The interface identifier + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No content if saving the configuration was successful + "400": + description: Bad Request + schema: + $ref: '#/definitions/model.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/model.Error' + summary: Save the interface configuration in wg-quick format to a file. + tags: + - Interface /interface/all: get: operationId: interfaces_handleAllGet @@ -762,16 +909,49 @@ paths: summary: Update the given peer record. tags: - Peer + /peer/config-mail: + post: + operationId: peers_handleEmailPost + parameters: + - description: The peer mail request data + in: body + name: request + required: true + schema: + $ref: '#/definitions/model.PeerMailRequest' + produces: + - application/json + responses: + "204": + description: No content if mail sending was successful + "400": + description: Bad Request + schema: + $ref: '#/definitions/model.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/model.Error' + summary: Send peer configuration via email. + tags: + - Peer /peer/config-qr/{id}: get: operationId: peers_handleQrCodeGet + parameters: + - description: The peer identifier + in: path + name: id + required: true + type: string produces: + - image/png - application/json responses: "200": description: OK schema: - type: string + type: file "400": description: Bad Request schema: @@ -786,6 +966,12 @@ paths: /peer/config/{id}: get: operationId: peers_handleConfigGet + parameters: + - description: The peer identifier + in: path + name: id + required: true + type: string produces: - application/json responses: @@ -833,6 +1019,41 @@ paths: summary: Get peers for the given interface. tags: - Peer + /peer/iface/{iface}/multiplenew: + post: + operationId: peers_handleCreateMultiplePost + parameters: + - description: The interface identifier + in: path + name: iface + required: true + type: string + - description: The peer creation request data + in: body + name: request + required: true + schema: + $ref: '#/definitions/model.MultiPeerRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/model.Peer' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/model.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/model.Error' + summary: Create multiple new peers for the given interface. + tags: + - Peer /peer/iface/{iface}/new: post: operationId: peers_handleCreatePost @@ -893,6 +1114,33 @@ paths: summary: Prepare a new peer for the given interface. tags: - Peer + /peer/iface/{iface}/stats: + get: + operationId: peers_handleStatsGet + parameters: + - description: The interface identifier + in: path + name: iface + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.PeerStats' + "400": + description: Bad Request + schema: + $ref: '#/definitions/model.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/model.Error' + summary: Get peer stats for the given interface. + tags: + - Peer /user/{id}: delete: operationId: users_handleDelete @@ -972,6 +1220,48 @@ paths: summary: Update the user record. tags: - Users + /user/{id}/api/disable: + post: + operationId: users_handleApiDisablePost + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.User' + "400": + description: Bad Request + schema: + $ref: '#/definitions/model.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/model.Error' + summary: Disable the REST API for the given user. + tags: + - Users + /user/{id}/api/enable: + post: + operationId: users_handleApiEnablePost + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.User' + "400": + description: Bad Request + schema: + $ref: '#/definitions/model.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/model.Error' + summary: Enable the REST API for the given user. + tags: + - Users /user/{id}/peers: get: operationId: users_handlePeersGet @@ -984,6 +1274,10 @@ paths: items: $ref: '#/definitions/model.Peer' type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/model.Error' "500": description: Internal Server Error schema: @@ -991,6 +1285,27 @@ paths: summary: Get peers for the given user. tags: - Users + /user/{id}/stats: + get: + operationId: users_handleStatsGet + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.PeerStats' + "400": + description: Bad Request + schema: + $ref: '#/definitions/model.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/model.Error' + summary: Get peer stats for the given user. + tags: + - Users /user/all: get: operationId: users_handleAllGet diff --git a/internal/app/api/core/assets/doc/v1_swagger.json b/internal/app/api/core/assets/doc/v1_swagger.json new file mode 100644 index 0000000..d23abeb --- /dev/null +++ b/internal/app/api/core/assets/doc/v1_swagger.json @@ -0,0 +1,1932 @@ +{ + "swagger": "2.0", + "info": { + "description": "The WireGuard Portal REST API enables efficient management of WireGuard VPN configurations through a set of JSON-based endpoints.\nIt supports creating and editing peers, interfaces, and user profiles, while also providing role-based access control and auditing.\nThis API allows seamless integration with external tools or scripts for automated network configuration and administration.", + "title": "WireGuard Portal Public API", + "contact": { + "name": "WireGuard Portal Project", + "url": "https://github.com/h44z/wg-portal" + }, + "license": { + "name": "MIT", + "url": "https://github.com/h44z/wg-portal/blob/master/LICENSE.txt" + }, + "version": "1.0" + }, + "basePath": "/api/v1", + "paths": { + "/interface/all": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Interfaces" + ], + "summary": "Get all interface records.", + "operationId": "interface_handleAllGet", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Interface" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/interface/by-id/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Interfaces" + ], + "summary": "Get a specific interface record by its identifier.", + "operationId": "interfaces_handleByIdGet", + "parameters": [ + { + "type": "string", + "description": "The interface identifier.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Interface" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + }, + "put": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Interfaces" + ], + "summary": "Update an interface record.", + "operationId": "interfaces_handleUpdatePut", + "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/models.Interface" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Interface" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + }, + "delete": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Interfaces" + ], + "summary": "Delete the interface record.", + "operationId": "interfaces_handleDelete", + "parameters": [ + { + "type": "string", + "description": "The interface identifier.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content if deletion was successful." + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/interface/new": { + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Interfaces" + ], + "summary": "Create a new interface record.", + "operationId": "interfaces_handleCreatePost", + "parameters": [ + { + "description": "The interface data.", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Interface" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Interface" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/peer/by-id/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Normal users can only access their own records. Admins can access all records.", + "produces": [ + "application/json" + ], + "tags": [ + "Peers" + ], + "summary": "Get a specific peer record by its identifier (public key).", + "operationId": "peers_handleByIdGet", + "parameters": [ + { + "type": "string", + "description": "The peer identifier (public key).", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Peer" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + }, + "put": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Only admins can update existing records.", + "produces": [ + "application/json" + ], + "tags": [ + "Peers" + ], + "summary": "Update a peer record.", + "operationId": "peers_handleUpdatePut", + "parameters": [ + { + "type": "string", + "description": "The peer identifier.", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "The peer data.", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Peer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Peer" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + }, + "delete": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Peers" + ], + "summary": "Delete the peer record.", + "operationId": "peers_handleDelete", + "parameters": [ + { + "type": "string", + "description": "The peer identifier.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content if deletion was successful." + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/peer/by-interface/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Peers" + ], + "summary": "Get all peer records for a given WireGuard interface.", + "operationId": "peers_handleAllForInterfaceGet", + "parameters": [ + { + "type": "string", + "description": "The WireGuard interface identifier.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Peer" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/peer/by-user/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Normal users can only access their own records. Admins can access all records.", + "produces": [ + "application/json" + ], + "tags": [ + "Peers" + ], + "summary": "Get all peer records for a given user.", + "operationId": "peers_handleAllForUserGet", + "parameters": [ + { + "type": "string", + "description": "The user identifier.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Peer" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/peer/new": { + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Only admins can create new records.", + "produces": [ + "application/json" + ], + "tags": [ + "Peers" + ], + "summary": "Create a new peer record.", + "operationId": "peers_handleCreatePost", + "parameters": [ + { + "description": "The peer data.", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Peer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Peer" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/provisioning/data/peer-config": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Normal users can only access their own record. Admins can access all records.", + "produces": [ + "text/plain", + "application/json" + ], + "tags": [ + "Provisioning" + ], + "summary": "Get the peer configuration in wg-quick format.", + "operationId": "provisioning_handlePeerConfigGet", + "parameters": [ + { + "type": "string", + "description": "The peer identifier (public key) that should be queried.", + "name": "PeerId", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "The WireGuard configuration file", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/provisioning/data/peer-qr": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Normal users can only access their own record. Admins can access all records.", + "produces": [ + "image/png", + "application/json" + ], + "tags": [ + "Provisioning" + ], + "summary": "Get the peer configuration as QR code.", + "operationId": "provisioning_handlePeerQrGet", + "parameters": [ + { + "type": "string", + "description": "The peer identifier (public key) that should be queried.", + "name": "PeerId", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "The WireGuard configuration QR code", + "schema": { + "type": "file" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/provisioning/data/user-info": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Normal users can only access their own record. Admins can access all records.", + "produces": [ + "application/json" + ], + "tags": [ + "Provisioning" + ], + "summary": "Get information about all peer records for a given user.", + "operationId": "provisioning_handleUserInfoGet", + "parameters": [ + { + "type": "string", + "description": "The user identifier that should be queried. If not set, the authenticated user is used.", + "name": "UserId", + "in": "query" + }, + { + "type": "string", + "description": "The email address that should be queried. If UserId is set, this is ignored.", + "name": "Email", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserInformation" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/provisioning/new-peer": { + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Normal users can only create new peers if self provisioning is allowed. Admins can always add new peers.", + "produces": [ + "application/json" + ], + "tags": [ + "Provisioning" + ], + "summary": "Create a new peer for the given interface and user.", + "operationId": "provisioning_handleNewPeerPost", + "parameters": [ + { + "description": "Provisioning request model.", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ProvisioningRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Peer" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/user/all": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get all user records.", + "operationId": "users_handleAllGet", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/user/by-id/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Normal users can only access their own record. Admins can access all records.", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Get a specific user record by its internal identifier.", + "operationId": "users_handleByIdGet", + "parameters": [ + { + "type": "string", + "description": "The user identifier.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + }, + "put": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Only admins can update existing records.", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Update a user record.", + "operationId": "users_handleUpdatePut", + "parameters": [ + { + "type": "string", + "description": "The user identifier.", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "The user data.", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.User" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + }, + "delete": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Delete the user record.", + "operationId": "users_handleDelete", + "parameters": [ + { + "type": "string", + "description": "The user identifier.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No content if deletion was successful." + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/user/new": { + "post": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "Only admins can create new records.", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Create a new user record.", + "operationId": "users_handleCreatePost", + "parameters": [ + { + "description": "The user data.", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.User" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + } + }, + "definitions": { + "models.ConfigOption-array_string": { + "type": "object", + "properties": { + "Overridable": { + "type": "boolean" + }, + "Value": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "models.ConfigOption-int": { + "type": "object", + "properties": { + "Overridable": { + "type": "boolean" + }, + "Value": { + "type": "integer" + } + } + }, + "models.ConfigOption-string": { + "type": "object", + "properties": { + "Overridable": { + "type": "boolean" + }, + "Value": { + "type": "string" + } + } + }, + "models.ConfigOption-uint32": { + "type": "object", + "properties": { + "Overridable": { + "type": "boolean" + }, + "Value": { + "type": "integer" + } + } + }, + "models.Error": { + "type": "object", + "properties": { + "Code": { + "description": "HTTP status code.", + "type": "integer" + }, + "Details": { + "description": "Additional error details.", + "type": "string" + }, + "Message": { + "description": "Error message.", + "type": "string" + } + } + }, + "models.ExpiryDate": { + "type": "object", + "properties": { + "time.Time": { + "type": "string" + } + } + }, + "models.Interface": { + "type": "object", + "required": [ + "Identifier", + "Mode", + "PrivateKey", + "PublicKey" + ], + "properties": { + "Addresses": { + "description": "Addresses is a list of IP addresses (in CIDR format) that are assigned to the interface.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "10.11.12.1/24" + ] + }, + "Disabled": { + "description": "Disabled is a flag that specifies if the interface is enabled (up) or not (down). Disabled interfaces are not able to accept connections.", + "type": "boolean", + "example": false + }, + "DisabledReason": { + "description": "DisabledReason is the reason why the interface has been disabled.", + "type": "string", + "example": "This is a reason why the interface has been disabled." + }, + "DisplayName": { + "description": "DisplayName is a nice display name / description for the interface.", + "type": "string", + "maxLength": 64, + "example": "My Interface" + }, + "Dns": { + "description": "Dns is a list of DNS servers that should be set if the interface is up.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "1.1.1.1" + ] + }, + "DnsSearch": { + "description": "DnsSearch is the dns search option string that should be set if the interface is up, will be appended to Dns servers.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "wg.local" + ] + }, + "EnabledPeers": { + "description": "EnabledPeers is the number of enabled peers for this interface. Only enabled peers are able to connect.", + "type": "integer", + "readOnly": true + }, + "FirewallMark": { + "description": "FirewallMark is an optional firewall mark which is used to handle interface traffic.", + "type": "integer" + }, + "Identifier": { + "description": "Identifier is the unique identifier of the interface. It is always equal to the device name of the interface.", + "type": "string", + "example": "wg0" + }, + "ListenPort": { + "description": "ListenPort is the listening port, for example: 51820. The listening port is only required for server interfaces.", + "type": "integer", + "maximum": 65535, + "minimum": 1, + "example": 51820 + }, + "Mode": { + "description": "Mode is the interface type, either 'server', 'client' or 'any'. The mode specifies how WireGuard Portal handles peers for this interface.", + "type": "string", + "enum": [ + "server", + "client", + "any" + ], + "example": "server" + }, + "Mtu": { + "description": "Mtu is the device MTU of the interface.", + "type": "integer", + "maximum": 9000, + "minimum": 1, + "example": 1420 + }, + "PeerDefAllowedIPs": { + "description": "PeerDefAllowedIPs specifies the default allowed IP addresses for a new peer.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "10.11.12.0/24" + ] + }, + "PeerDefDns": { + "description": "PeerDefDns specifies the default dns servers for a new peer.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "8.8.8.8" + ] + }, + "PeerDefDnsSearch": { + "description": "PeerDefDnsSearch specifies the default dns search options for a new peer.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "wg.local" + ] + }, + "PeerDefEndpoint": { + "description": "PeerDefEndpoint specifies the default endpoint for a new peer.", + "type": "string", + "example": "wg.example.com:51820" + }, + "PeerDefFirewallMark": { + "description": "PeerDefFirewallMark specifies the default firewall mark for a new peer.", + "type": "integer" + }, + "PeerDefMtu": { + "description": "PeerDefMtu specifies the default device MTU for a new peer.", + "type": "integer", + "example": 1420 + }, + "PeerDefNetwork": { + "description": "PeerDefNetwork specifies the default subnets from which new peers will get their IP addresses. The subnet is specified in CIDR format.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "10.11.12.0/24" + ] + }, + "PeerDefPersistentKeepalive": { + "description": "PeerDefPersistentKeepalive specifies the default persistent keep-alive value in seconds for a new peer.", + "type": "integer", + "example": 25 + }, + "PeerDefPostDown": { + "description": "PeerDefPostDown specifies the default action that is executed after the device is down for a new peer.", + "type": "string" + }, + "PeerDefPostUp": { + "description": "PeerDefPostUp specifies the default action that is executed after the device is up for a new peer.", + "type": "string" + }, + "PeerDefPreDown": { + "description": "PeerDefPreDown specifies the default action that is executed before the device is down for a new peer.", + "type": "string" + }, + "PeerDefPreUp": { + "description": "PeerDefPreUp specifies the default action that is executed before the device is up for a new peer.", + "type": "string" + }, + "PeerDefRoutingTable": { + "description": "PeerDefRoutingTable specifies the default routing table for a new peer.", + "type": "string" + }, + "PostDown": { + "description": "PostDown is an optional action that is executed after the device is down.", + "type": "string", + "example": "echo 'Interface is down'" + }, + "PostUp": { + "description": "PostUp is an optional action that is executed after the device is up.", + "type": "string", + "example": "iptables -A FORWARD -i %i -j ACCEPT" + }, + "PreDown": { + "description": "PreDown is an optional action that is executed before the device is down.", + "type": "string", + "example": "iptables -D FORWARD -i %i -j ACCEPT" + }, + "PreUp": { + "description": "PreUp is an optional action that is executed before the device is up.", + "type": "string", + "example": "echo 'Interface is up'" + }, + "PrivateKey": { + "description": "PrivateKey is the private key of the interface.", + "type": "string", + "example": "gI6EdUSYvn8ugXOt8QQD6Yc+JyiZxIhp3GInSWRfWGE=" + }, + "PublicKey": { + "description": "PublicKey is the public key of the server interface. The public key is used by peers to connect to the server.", + "type": "string", + "example": "HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=" + }, + "RoutingTable": { + "description": "RoutingTable is an optional routing table which is used to route interface traffic.", + "type": "string" + }, + "SaveConfig": { + "description": "SaveConfig is a flag that specifies if the configuration should be saved to the configuration file (wgX.conf in wg-quick format).", + "type": "boolean", + "example": false + }, + "TotalPeers": { + "description": "TotalPeers is the total number of peers for this interface.", + "type": "integer", + "readOnly": true + } + } + }, + "models.Peer": { + "type": "object", + "required": [ + "Identifier", + "InterfaceIdentifier", + "PrivateKey" + ], + "properties": { + "Addresses": { + "description": "Addresses is a list of IP addresses in CIDR format (both IPv4 and IPv6) for the peer.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "10.11.12.2/24" + ] + }, + "AllowedIPs": { + "description": "AllowedIPs is a list of allowed IP subnets for the peer.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-array_string" + } + ] + }, + "CheckAliveAddress": { + "description": "CheckAliveAddress is an optional ip address or DNS name that is used for ping checks.", + "type": "string", + "example": "1.1.1.1" + }, + "Disabled": { + "description": "Disabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.", + "type": "boolean", + "example": false + }, + "DisabledReason": { + "description": "DisabledReason is the reason why the peer has been disabled.", + "type": "string", + "example": "This is a reason why the peer has been disabled." + }, + "DisplayName": { + "description": "DisplayName is a nice display name / description for the peer.", + "type": "string", + "maxLength": 64, + "example": "My Peer" + }, + "Dns": { + "description": "Dns is a list of DNS servers that should be set if the peer interface is up.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-array_string" + } + ] + }, + "DnsSearch": { + "description": "DnsSearch is the dns search option string that should be set if the peer interface is up, will be appended to Dns servers.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-array_string" + } + ] + }, + "Endpoint": { + "description": "Endpoint is the endpoint address of the peer.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-string" + } + ] + }, + "EndpointPublicKey": { + "description": "EndpointPublicKey is the endpoint public key.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-string" + } + ] + }, + "ExpiresAt": { + "description": "ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. An expired peer is not able to connect.", + "allOf": [ + { + "$ref": "#/definitions/models.ExpiryDate" + } + ] + }, + "ExtraAllowedIPs": { + "description": "ExtraAllowedIPs is a list of additional allowed IP subnets for the peer. These allowed IP subnets are added on the server side.", + "type": "array", + "items": { + "type": "string" + } + }, + "FirewallMark": { + "description": "FirewallMark is an optional firewall mark which is used to handle peer traffic.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-uint32" + } + ] + }, + "Identifier": { + "description": "Identifier is the unique identifier of the peer. It is always equal to the public key of the peer.", + "type": "string", + "example": "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" + }, + "InterfaceIdentifier": { + "description": "InterfaceIdentifier is the identifier of the interface the peer is linked to.", + "type": "string", + "example": "wg0" + }, + "Mode": { + "description": "Mode is the peer interface type (server, client, any).", + "type": "string", + "enum": [ + "server", + "client", + "any" + ], + "example": "client" + }, + "Mtu": { + "description": "Mtu is the device MTU of the peer.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-int" + } + ] + }, + "Notes": { + "description": "Notes is a note field for peers.", + "type": "string", + "example": "This is a note for the peer." + }, + "PersistentKeepalive": { + "description": "PersistentKeepalive is the optional persistent keep-alive interval in seconds.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-int" + } + ] + }, + "PostDown": { + "description": "PostDown is an optional action that is executed after the device is down.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-string" + } + ] + }, + "PostUp": { + "description": "PostUp is an optional action that is executed after the device is up.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-string" + } + ] + }, + "PreDown": { + "description": "PreDown is an optional action that is executed before the device is down.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-string" + } + ] + }, + "PreUp": { + "description": "PreUp is an optional action that is executed before the device is up.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-string" + } + ] + }, + "PresharedKey": { + "description": "PresharedKey is the optional pre-shared Key of the peer.", + "type": "string", + "example": "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" + }, + "PrivateKey": { + "description": "PrivateKey is the private Key of the peer.", + "type": "string", + "example": "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" + }, + "PublicKey": { + "description": "PublicKey is the public Key of the server peer.", + "type": "string", + "example": "TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=" + }, + "RoutingTable": { + "description": "RoutingTable is an optional routing table which is used to route peer traffic.", + "allOf": [ + { + "$ref": "#/definitions/models.ConfigOption-string" + } + ] + }, + "UserIdentifier": { + "description": "UserIdentifier is the identifier of the user that owns the peer.", + "type": "string", + "example": "uid-1234567" + } + } + }, + "models.ProvisioningRequest": { + "type": "object", + "required": [ + "InterfaceIdentifier" + ], + "properties": { + "InterfaceIdentifier": { + "description": "InterfaceIdentifier is the identifier of the WireGuard interface the peer should be linked to.", + "type": "string", + "example": "wg0" + }, + "PresharedKey": { + "description": "PresharedKey is the optional pre-shared key of the peer. If no pre-shared key is set, a new key is generated.", + "type": "string", + "example": "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" + }, + "PublicKey": { + "description": "PublicKey is the optional public key of the peer. If no public key is set, a new key pair is generated.", + "type": "string", + "example": "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" + }, + "UserIdentifier": { + "description": "UserIdentifier is the identifier of the user the peer should be linked to.\nIf no user identifier is set, the authenticated user is used.", + "type": "string", + "example": "uid-1234567" + } + } + }, + "models.User": { + "type": "object", + "required": [ + "Identifier", + "IsAdmin" + ], + "properties": { + "ApiEnabled": { + "description": "If this field is set, the user is allowed to use the RESTful API. This field is read-only.", + "type": "boolean", + "readOnly": true, + "example": false + }, + "ApiToken": { + "description": "The API token of the user. This field is never populated on bulk read operations.", + "type": "string", + "maxLength": 64, + "minLength": 32, + "example": "" + }, + "Department": { + "description": "The department of the user. This field is optional.", + "type": "string", + "example": "Software Development" + }, + "Disabled": { + "description": "If this field is set, the user is disabled.", + "type": "boolean", + "example": false + }, + "DisabledReason": { + "description": "The reason why the user has been disabled.", + "type": "string", + "example": "" + }, + "Email": { + "description": "The email address of the user. This field is optional.", + "type": "string", + "example": "test@test.com" + }, + "Firstname": { + "description": "The first name of the user. This field is optional.", + "type": "string", + "example": "Max" + }, + "Identifier": { + "description": "The unique identifier of the user.", + "type": "string", + "maxLength": 64, + "example": "uid-1234567" + }, + "IsAdmin": { + "description": "If this field is set, the user is an admin.", + "type": "boolean", + "example": false + }, + "Lastname": { + "description": "The last name of the user. This field is optional.", + "type": "string", + "example": "Muster" + }, + "Locked": { + "description": "If this field is set, the user is locked and thus unable to log in to WireGuard Portal.", + "type": "boolean", + "example": false + }, + "LockedReason": { + "description": "The reason why the user has been locked.", + "type": "string", + "example": "" + }, + "Notes": { + "description": "Additional notes about the user. This field is optional.", + "type": "string", + "example": "some sample notes" + }, + "Password": { + "description": "The password of the user. This field is never populated on read operations.", + "type": "string", + "maxLength": 64, + "minLength": 16, + "example": "" + }, + "PeerCount": { + "description": "The number of peers linked to the user. This field is read-only.", + "type": "integer", + "readOnly": true, + "example": 2 + }, + "Phone": { + "description": "The phone number of the user. This field is optional.", + "type": "string", + "example": "+1234546789" + }, + "ProviderName": { + "description": "The name of the authentication provider. This field is read-only.", + "type": "string", + "readOnly": true, + "example": "" + }, + "Source": { + "description": "The source of the user. This field is optional.", + "type": "string", + "enum": [ + "db" + ], + "example": "db" + } + } + }, + "models.UserInformation": { + "type": "object", + "properties": { + "PeerCount": { + "description": "PeerCount is the number of peers linked to the user.", + "type": "integer", + "example": 2 + }, + "Peers": { + "description": "Peers is a list of peers linked to the user.", + "type": "array", + "items": { + "$ref": "#/definitions/models.UserInformationPeer" + } + }, + "UserIdentifier": { + "description": "UserIdentifier is the unique identifier of the user.", + "type": "string", + "example": "uid-1234567" + } + } + }, + "models.UserInformationPeer": { + "type": "object", + "properties": { + "DisplayName": { + "description": "DisplayName is a user-defined description of the peer.", + "type": "string", + "example": "My iPhone" + }, + "Identifier": { + "description": "Identifier is the unique identifier of the peer. It equals the public key of the peer.", + "type": "string", + "example": "peer-1234567" + }, + "InterfaceIdentifier": { + "description": "InterfaceIdentifier is the unique identifier of the WireGuard Portal device the peer is connected to.", + "type": "string", + "example": "wg0" + }, + "IpAddresses": { + "description": "IPAddresses is a list of IP addresses in CIDR format assigned to the peer.", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "10.11.12.2/24" + ] + }, + "IsDisabled": { + "description": "IsDisabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.", + "type": "boolean", + "example": true + } + } + } + }, + "securityDefinitions": { + "BasicAuth": { + "type": "basic" + } + } +} \ No newline at end of file diff --git a/internal/app/api/core/assets/doc/v1_swagger.yaml b/internal/app/api/core/assets/doc/v1_swagger.yaml new file mode 100644 index 0000000..00c914f --- /dev/null +++ b/internal/app/api/core/assets/doc/v1_swagger.yaml @@ -0,0 +1,1358 @@ +basePath: /api/v1 +definitions: + models.ConfigOption-array_string: + properties: + Overridable: + type: boolean + Value: + items: + type: string + type: array + type: object + models.ConfigOption-int: + properties: + Overridable: + type: boolean + Value: + type: integer + type: object + models.ConfigOption-string: + properties: + Overridable: + type: boolean + Value: + type: string + type: object + models.ConfigOption-uint32: + properties: + Overridable: + type: boolean + Value: + type: integer + type: object + models.Error: + properties: + Code: + description: HTTP status code. + type: integer + Details: + description: Additional error details. + type: string + Message: + description: Error message. + type: string + type: object + models.ExpiryDate: + properties: + time.Time: + type: string + type: object + models.Interface: + properties: + Addresses: + description: Addresses is a list of IP addresses (in CIDR format) that are + assigned to the interface. + example: + - 10.11.12.1/24 + items: + type: string + type: array + Disabled: + description: Disabled is a flag that specifies if the interface is enabled + (up) or not (down). Disabled interfaces are not able to accept connections. + example: false + type: boolean + DisabledReason: + description: DisabledReason is the reason why the interface has been disabled. + example: This is a reason why the interface has been disabled. + type: string + DisplayName: + description: DisplayName is a nice display name / description for the interface. + example: My Interface + maxLength: 64 + type: string + Dns: + description: Dns is a list of DNS servers that should be set if the interface + is up. + example: + - 1.1.1.1 + items: + type: string + type: array + DnsSearch: + description: DnsSearch is the dns search option string that should be set + if the interface is up, will be appended to Dns servers. + example: + - wg.local + items: + type: string + type: array + EnabledPeers: + description: EnabledPeers is the number of enabled peers for this interface. + Only enabled peers are able to connect. + readOnly: true + type: integer + FirewallMark: + description: FirewallMark is an optional firewall mark which is used to handle + interface traffic. + type: integer + Identifier: + description: Identifier is the unique identifier of the interface. It is always + equal to the device name of the interface. + example: wg0 + type: string + ListenPort: + description: 'ListenPort is the listening port, for example: 51820. The listening + port is only required for server interfaces.' + example: 51820 + maximum: 65535 + minimum: 1 + type: integer + Mode: + description: Mode is the interface type, either 'server', 'client' or 'any'. + The mode specifies how WireGuard Portal handles peers for this interface. + enum: + - server + - client + - any + example: server + type: string + Mtu: + description: Mtu is the device MTU of the interface. + example: 1420 + maximum: 9000 + minimum: 1 + type: integer + PeerDefAllowedIPs: + description: PeerDefAllowedIPs specifies the default allowed IP addresses + for a new peer. + example: + - 10.11.12.0/24 + items: + type: string + type: array + PeerDefDns: + description: PeerDefDns specifies the default dns servers for a new peer. + example: + - 8.8.8.8 + items: + type: string + type: array + PeerDefDnsSearch: + description: PeerDefDnsSearch specifies the default dns search options for + a new peer. + example: + - wg.local + items: + type: string + type: array + PeerDefEndpoint: + description: PeerDefEndpoint specifies the default endpoint for a new peer. + example: wg.example.com:51820 + type: string + PeerDefFirewallMark: + description: PeerDefFirewallMark specifies the default firewall mark for a + new peer. + type: integer + PeerDefMtu: + description: PeerDefMtu specifies the default device MTU for a new peer. + example: 1420 + type: integer + PeerDefNetwork: + description: PeerDefNetwork specifies the default subnets from which new peers + will get their IP addresses. The subnet is specified in CIDR format. + example: + - 10.11.12.0/24 + items: + type: string + type: array + PeerDefPersistentKeepalive: + description: PeerDefPersistentKeepalive specifies the default persistent keep-alive + value in seconds for a new peer. + example: 25 + type: integer + PeerDefPostDown: + description: PeerDefPostDown specifies the default action that is executed + after the device is down for a new peer. + type: string + PeerDefPostUp: + description: PeerDefPostUp specifies the default action that is executed after + the device is up for a new peer. + type: string + PeerDefPreDown: + description: PeerDefPreDown specifies the default action that is executed + before the device is down for a new peer. + type: string + PeerDefPreUp: + description: PeerDefPreUp specifies the default action that is executed before + the device is up for a new peer. + type: string + PeerDefRoutingTable: + description: PeerDefRoutingTable specifies the default routing table for a + new peer. + type: string + PostDown: + description: PostDown is an optional action that is executed after the device + is down. + example: echo 'Interface is down' + type: string + PostUp: + description: PostUp is an optional action that is executed after the device + is up. + example: iptables -A FORWARD -i %i -j ACCEPT + type: string + PreDown: + description: PreDown is an optional action that is executed before the device + is down. + example: iptables -D FORWARD -i %i -j ACCEPT + type: string + PreUp: + description: PreUp is an optional action that is executed before the device + is up. + example: echo 'Interface is up' + type: string + PrivateKey: + description: PrivateKey is the private key of the interface. + example: gI6EdUSYvn8ugXOt8QQD6Yc+JyiZxIhp3GInSWRfWGE= + type: string + PublicKey: + description: PublicKey is the public key of the server interface. The public + key is used by peers to connect to the server. + example: HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw= + type: string + RoutingTable: + description: RoutingTable is an optional routing table which is used to route + interface traffic. + type: string + SaveConfig: + description: SaveConfig is a flag that specifies if the configuration should + be saved to the configuration file (wgX.conf in wg-quick format). + example: false + type: boolean + TotalPeers: + description: TotalPeers is the total number of peers for this interface. + readOnly: true + type: integer + required: + - Identifier + - Mode + - PrivateKey + - PublicKey + type: object + models.Peer: + properties: + Addresses: + description: Addresses is a list of IP addresses in CIDR format (both IPv4 + and IPv6) for the peer. + example: + - 10.11.12.2/24 + items: + type: string + type: array + AllowedIPs: + allOf: + - $ref: '#/definitions/models.ConfigOption-array_string' + description: AllowedIPs is a list of allowed IP subnets for the peer. + CheckAliveAddress: + description: CheckAliveAddress is an optional ip address or DNS name that + is used for ping checks. + example: 1.1.1.1 + type: string + Disabled: + description: Disabled is a flag that specifies if the peer is enabled or not. + Disabled peers are not able to connect. + example: false + type: boolean + DisabledReason: + description: DisabledReason is the reason why the peer has been disabled. + example: This is a reason why the peer has been disabled. + type: string + DisplayName: + description: DisplayName is a nice display name / description for the peer. + example: My Peer + maxLength: 64 + type: string + Dns: + allOf: + - $ref: '#/definitions/models.ConfigOption-array_string' + description: Dns is a list of DNS servers that should be set if the peer interface + is up. + DnsSearch: + allOf: + - $ref: '#/definitions/models.ConfigOption-array_string' + description: DnsSearch is the dns search option string that should be set + if the peer interface is up, will be appended to Dns servers. + Endpoint: + allOf: + - $ref: '#/definitions/models.ConfigOption-string' + description: Endpoint is the endpoint address of the peer. + EndpointPublicKey: + allOf: + - $ref: '#/definitions/models.ConfigOption-string' + description: EndpointPublicKey is the endpoint public key. + ExpiresAt: + allOf: + - $ref: '#/definitions/models.ExpiryDate' + description: ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. + An expired peer is not able to connect. + ExtraAllowedIPs: + description: ExtraAllowedIPs is a list of additional allowed IP subnets for + the peer. These allowed IP subnets are added on the server side. + items: + type: string + type: array + FirewallMark: + allOf: + - $ref: '#/definitions/models.ConfigOption-uint32' + description: FirewallMark is an optional firewall mark which is used to handle + peer traffic. + Identifier: + description: Identifier is the unique identifier of the peer. It is always + equal to the public key of the peer. + example: xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg= + type: string + InterfaceIdentifier: + description: InterfaceIdentifier is the identifier of the interface the peer + is linked to. + example: wg0 + type: string + Mode: + description: Mode is the peer interface type (server, client, any). + enum: + - server + - client + - any + example: client + type: string + Mtu: + allOf: + - $ref: '#/definitions/models.ConfigOption-int' + description: Mtu is the device MTU of the peer. + Notes: + description: Notes is a note field for peers. + example: This is a note for the peer. + type: string + PersistentKeepalive: + allOf: + - $ref: '#/definitions/models.ConfigOption-int' + description: PersistentKeepalive is the optional persistent keep-alive interval + in seconds. + PostDown: + allOf: + - $ref: '#/definitions/models.ConfigOption-string' + description: PostDown is an optional action that is executed after the device + is down. + PostUp: + allOf: + - $ref: '#/definitions/models.ConfigOption-string' + description: PostUp is an optional action that is executed after the device + is up. + PreDown: + allOf: + - $ref: '#/definitions/models.ConfigOption-string' + description: PreDown is an optional action that is executed before the device + is down. + PreUp: + allOf: + - $ref: '#/definitions/models.ConfigOption-string' + description: PreUp is an optional action that is executed before the device + is up. + PresharedKey: + description: PresharedKey is the optional pre-shared Key of the peer. + example: yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= + type: string + PrivateKey: + description: PrivateKey is the private Key of the peer. + example: yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= + type: string + PublicKey: + description: PublicKey is the public Key of the server peer. + example: TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0= + type: string + RoutingTable: + allOf: + - $ref: '#/definitions/models.ConfigOption-string' + description: RoutingTable is an optional routing table which is used to route + peer traffic. + UserIdentifier: + description: UserIdentifier is the identifier of the user that owns the peer. + example: uid-1234567 + type: string + required: + - Identifier + - InterfaceIdentifier + - PrivateKey + type: object + models.ProvisioningRequest: + properties: + InterfaceIdentifier: + description: InterfaceIdentifier is the identifier of the WireGuard interface + the peer should be linked to. + example: wg0 + type: string + PresharedKey: + description: PresharedKey is the optional pre-shared key of the peer. If no + pre-shared key is set, a new key is generated. + example: yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= + type: string + PublicKey: + description: PublicKey is the optional public key of the peer. If no public + key is set, a new key pair is generated. + example: xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg= + type: string + UserIdentifier: + description: |- + UserIdentifier is the identifier of the user the peer should be linked to. + If no user identifier is set, the authenticated user is used. + example: uid-1234567 + type: string + required: + - InterfaceIdentifier + type: object + models.User: + properties: + ApiEnabled: + description: If this field is set, the user is allowed to use the RESTful + API. This field is read-only. + example: false + readOnly: true + type: boolean + ApiToken: + description: The API token of the user. This field is never populated on bulk + read operations. + example: "" + maxLength: 64 + minLength: 32 + type: string + Department: + description: The department of the user. This field is optional. + example: Software Development + type: string + Disabled: + description: If this field is set, the user is disabled. + example: false + type: boolean + DisabledReason: + description: The reason why the user has been disabled. + example: "" + type: string + Email: + description: The email address of the user. This field is optional. + example: test@test.com + type: string + Firstname: + description: The first name of the user. This field is optional. + example: Max + type: string + Identifier: + description: The unique identifier of the user. + example: uid-1234567 + maxLength: 64 + type: string + IsAdmin: + description: If this field is set, the user is an admin. + example: false + type: boolean + Lastname: + description: The last name of the user. This field is optional. + example: Muster + type: string + Locked: + description: If this field is set, the user is locked and thus unable to log + in to WireGuard Portal. + example: false + type: boolean + LockedReason: + description: The reason why the user has been locked. + example: "" + type: string + Notes: + description: Additional notes about the user. This field is optional. + example: some sample notes + type: string + Password: + description: The password of the user. This field is never populated on read + operations. + example: "" + maxLength: 64 + minLength: 16 + type: string + PeerCount: + description: The number of peers linked to the user. This field is read-only. + example: 2 + readOnly: true + type: integer + Phone: + description: The phone number of the user. This field is optional. + example: "+1234546789" + type: string + ProviderName: + description: The name of the authentication provider. This field is read-only. + example: "" + readOnly: true + type: string + Source: + description: The source of the user. This field is optional. + enum: + - db + example: db + type: string + required: + - Identifier + - IsAdmin + type: object + models.UserInformation: + properties: + PeerCount: + description: PeerCount is the number of peers linked to the user. + example: 2 + type: integer + Peers: + description: Peers is a list of peers linked to the user. + items: + $ref: '#/definitions/models.UserInformationPeer' + type: array + UserIdentifier: + description: UserIdentifier is the unique identifier of the user. + example: uid-1234567 + type: string + type: object + models.UserInformationPeer: + properties: + DisplayName: + description: DisplayName is a user-defined description of the peer. + example: My iPhone + type: string + Identifier: + description: Identifier is the unique identifier of the peer. It equals the + public key of the peer. + example: peer-1234567 + type: string + InterfaceIdentifier: + description: InterfaceIdentifier is the unique identifier of the WireGuard + Portal device the peer is connected to. + example: wg0 + type: string + IpAddresses: + description: IPAddresses is a list of IP addresses in CIDR format assigned + to the peer. + example: + - 10.11.12.2/24 + items: + type: string + type: array + IsDisabled: + description: IsDisabled is a flag that specifies if the peer is enabled or + not. Disabled peers are not able to connect. + example: true + type: boolean + type: object +info: + contact: + name: WireGuard Portal Project + url: https://github.com/h44z/wg-portal + description: |- + The WireGuard Portal REST API enables efficient management of WireGuard VPN configurations through a set of JSON-based endpoints. + It supports creating and editing peers, interfaces, and user profiles, while also providing role-based access control and auditing. + This API allows seamless integration with external tools or scripts for automated network configuration and administration. + license: + name: MIT + url: https://github.com/h44z/wg-portal/blob/master/LICENSE.txt + title: WireGuard Portal Public API + version: "1.0" +paths: + /interface/all: + get: + operationId: interface_handleAllGet + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Interface' + type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get all interface records. + tags: + - Interfaces + /interface/by-id/{id}: + delete: + operationId: interfaces_handleDelete + parameters: + - description: The interface identifier. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No content if deletion was successful. + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Delete the interface record. + tags: + - Interfaces + get: + operationId: interfaces_handleByIdGet + parameters: + - description: The interface identifier. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Interface' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get a specific interface record by its identifier. + tags: + - Interfaces + put: + operationId: interfaces_handleUpdatePut + 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/models.Interface' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Interface' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Update an interface record. + tags: + - Interfaces + /interface/new: + post: + operationId: interfaces_handleCreatePost + parameters: + - description: The interface data. + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.Interface' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Interface' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "409": + description: Conflict + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Create a new interface record. + tags: + - Interfaces + /peer/by-id/{id}: + delete: + operationId: peers_handleDelete + parameters: + - description: The peer identifier. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No content if deletion was successful. + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Delete the peer record. + tags: + - Peers + get: + description: Normal users can only access their own records. Admins can access + all records. + operationId: peers_handleByIdGet + parameters: + - description: The peer identifier (public key). + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Peer' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get a specific peer record by its identifier (public key). + tags: + - Peers + put: + description: Only admins can update existing records. + operationId: peers_handleUpdatePut + parameters: + - description: The peer identifier. + in: path + name: id + required: true + type: string + - description: The peer data. + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.Peer' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Peer' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Update a peer record. + tags: + - Peers + /peer/by-interface/{id}: + get: + operationId: peers_handleAllForInterfaceGet + parameters: + - description: The WireGuard interface identifier. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Peer' + type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get all peer records for a given WireGuard interface. + tags: + - Peers + /peer/by-user/{id}: + get: + description: Normal users can only access their own records. Admins can access + all records. + operationId: peers_handleAllForUserGet + parameters: + - description: The user identifier. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Peer' + type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get all peer records for a given user. + tags: + - Peers + /peer/new: + post: + description: Only admins can create new records. + operationId: peers_handleCreatePost + parameters: + - description: The peer data. + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.Peer' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Peer' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "409": + description: Conflict + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Create a new peer record. + tags: + - Peers + /provisioning/data/peer-config: + get: + description: Normal users can only access their own record. Admins can access + all records. + operationId: provisioning_handlePeerConfigGet + parameters: + - description: The peer identifier (public key) that should be queried. + in: query + name: PeerId + required: true + type: string + produces: + - text/plain + - application/json + responses: + "200": + description: The WireGuard configuration file + schema: + type: string + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get the peer configuration in wg-quick format. + tags: + - Provisioning + /provisioning/data/peer-qr: + get: + description: Normal users can only access their own record. Admins can access + all records. + operationId: provisioning_handlePeerQrGet + parameters: + - description: The peer identifier (public key) that should be queried. + in: query + name: PeerId + required: true + type: string + produces: + - image/png + - application/json + responses: + "200": + description: The WireGuard configuration QR code + schema: + type: file + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get the peer configuration as QR code. + tags: + - Provisioning + /provisioning/data/user-info: + get: + description: Normal users can only access their own record. Admins can access + all records. + operationId: provisioning_handleUserInfoGet + parameters: + - description: The user identifier that should be queried. If not set, the authenticated + user is used. + in: query + name: UserId + type: string + - description: The email address that should be queried. If UserId is set, this + is ignored. + in: query + name: Email + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.UserInformation' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get information about all peer records for a given user. + tags: + - Provisioning + /provisioning/new-peer: + post: + description: Normal users can only create new peers if self provisioning is + allowed. Admins can always add new peers. + operationId: provisioning_handleNewPeerPost + parameters: + - description: Provisioning request model. + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.ProvisioningRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Peer' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Create a new peer for the given interface and user. + tags: + - Provisioning + /user/all: + get: + operationId: users_handleAllGet + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.User' + type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get all user records. + tags: + - Users + /user/by-id/{id}: + delete: + operationId: users_handleDelete + parameters: + - description: The user identifier. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No content if deletion was successful. + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Delete the user record. + tags: + - Users + get: + description: Normal users can only access their own record. Admins can access + all records. + operationId: users_handleByIdGet + parameters: + - description: The user identifier. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get a specific user record by its internal identifier. + tags: + - Users + put: + description: Only admins can update existing records. + operationId: users_handleUpdatePut + parameters: + - description: The user identifier. + in: path + name: id + required: true + type: string + - description: The user data. + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.User' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Update a user record. + tags: + - Users + /user/new: + post: + description: Only admins can create new records. + operationId: users_handleCreatePost + parameters: + - description: The user data. + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.User' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "409": + description: Conflict + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Create a new user record. + tags: + - Users +securityDefinitions: + BasicAuth: + type: basic +swagger: "2.0" diff --git a/internal/app/api/core/assets/js/rapidoc-min.js b/internal/app/api/core/assets/js/rapidoc-min.js index 6a28927..c656086 100644 --- a/internal/app/api/core/assets/js/rapidoc-min.js +++ b/internal/app/api/core/assets/js/rapidoc-min.js @@ -1,2 +1,3915 @@ -/*! RapiDoc 9.1.8 | Author - Mrinmoy Majumdar | License information can be found in rapidoc-min.js.LICENSE.txt */ -(()=>{var e,t,r={310:(e,t,r)=>{"use strict";const n=new WeakMap,a=e=>(...t)=>{const r=e(...t);return n.set(r,!0),r},o=e=>"function"==typeof e&&n.has(e),i="undefined"!=typeof window&&null!=window.customElements&&void 0!==window.customElements.polyfillWrapFlushCallback,s=(e,t,r=null)=>{for(;t!==r;){const r=t.nextSibling;e.removeChild(t),t=r}},l={},c={},p=`{{lit-${String(Math.random()).slice(2)}}}`,u=`\x3c!--${p}--\x3e`,d=new RegExp(`${p}|${u}`),h="$lit$";class f{constructor(e,t){this.parts=[],this.element=t;const r=[],n=[],a=document.createTreeWalker(t.content,133,null,!1);let o=0,i=-1,s=0;const{strings:l,values:{length:c}}=e;for(;s0;){const t=l[s],r=v.exec(t)[2],n=r.toLowerCase()+h,a=e.getAttribute(n);e.removeAttribute(n);const o=a.split(d);this.parts.push({type:"attribute",index:i,name:r,strings:o}),s+=o.length-1}}"TEMPLATE"===e.tagName&&(n.push(e),a.currentNode=e.content)}else if(3===e.nodeType){const t=e.data;if(t.indexOf(p)>=0){const n=e.parentNode,a=t.split(d),o=a.length-1;for(let t=0;t{const r=e.length-t.length;return r>=0&&e.slice(r)===t},y=e=>-1!==e.index,g=()=>document.createComment(""),v=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;class b{constructor(e,t,r){this.__parts=[],this.template=e,this.processor=t,this.options=r}update(e){let t=0;for(const r of this.__parts)void 0!==r&&r.setValue(e[t]),t++;for(const e of this.__parts)void 0!==e&&e.commit()}_clone(){const e=i?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),t=[],r=this.template.parts,n=document.createTreeWalker(e,133,null,!1);let a,o=0,s=0,l=n.nextNode();for(;oe}),w=` ${p} `;class S{constructor(e,t,r,n){this.strings=e,this.values=t,this.type=r,this.processor=n}getHTML(){const e=this.strings.length-1;let t="",r=!1;for(let n=0;n-1||r)&&-1===e.indexOf("--\x3e",a+1);const o=v.exec(e);t+=null===o?e+(r?w:u):e.substr(0,o.index)+o[1]+o[2]+h+o[3]+p}return t+=this.strings[e],t}getTemplateElement(){const e=document.createElement("template");let t=this.getHTML();return void 0!==x&&(t=x.createHTML(t)),e.innerHTML=t,e}}const k=e=>null===e||!("object"==typeof e||"function"==typeof e),$=e=>Array.isArray(e)||!(!e||!e[Symbol.iterator]);class A{constructor(e,t,r){this.dirty=!0,this.element=e,this.name=t,this.strings=r,this.parts=[];for(let e=0;e{try{const e={get capture(){return j=!0,!1}};window.addEventListener("test",e,e),window.removeEventListener("test",e,e)}catch(e){}})();class I{constructor(e,t,r){this.value=void 0,this.__pendingValue=void 0,this.element=e,this.eventName=t,this.eventContext=r,this.__boundHandleEvent=e=>this.handleEvent(e)}setValue(e){this.__pendingValue=e}commit(){for(;o(this.__pendingValue);){const e=this.__pendingValue;this.__pendingValue=l,e(this)}if(this.__pendingValue===l)return;const e=this.__pendingValue,t=this.value,r=null==e||null!=t&&(e.capture!==t.capture||e.once!==t.once||e.passive!==t.passive),n=null!=e&&(null==t||r);r&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),n&&(this.__options=P(e),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=e,this.__pendingValue=l}handleEvent(e){"function"==typeof this.value?this.value.call(this.eventContext||this.element,e):this.value.handleEvent(e)}}const P=e=>e&&(j?{capture:e.capture,passive:e.passive,once:e.once}:e.capture);const R=new class{handleAttributeExpressions(e,t,r,n){const a=t[0];if("."===a){return new _(e,t.slice(1),r).parts}if("@"===a)return[new I(e,t.slice(1),n.eventContext)];if("?"===a)return[new T(e,t.slice(1),r)];return new A(e,t,r).parts}handleTextExpression(e){return new O(e)}};function L(e){let t=N.get(e.type);void 0===t&&(t={stringsArray:new WeakMap,keyString:new Map},N.set(e.type,t));let r=t.stringsArray.get(e.strings);if(void 0!==r)return r;const n=e.strings.join(p);return r=t.keyString.get(n),void 0===r&&(r=new f(e,e.getTemplateElement()),t.keyString.set(n,r)),t.stringsArray.set(e.strings,r),r}const N=new Map,F=new WeakMap;"undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.4.1");const D=(e,...t)=>new S(e,t,"html",R);window.JSCompiler_renameProperty=(e,t)=>e;const B={toAttribute(e,t){switch(t){case Boolean:return e?"":null;case Object:case Array:return null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){switch(t){case Boolean:return null!==e;case Number:return null===e?null:Number(e);case Object:case Array:return JSON.parse(e)}return e}},z=(e,t)=>t!==e&&(t==t||e==e),q={attribute:!0,type:String,converter:B,reflect:!1,hasChanged:z},U="finalized";class M extends HTMLElement{constructor(){super(),this.initialize()}static get observedAttributes(){this.finalize();const e=[];return this._classProperties.forEach(((t,r)=>{const n=this._attributeNameForProperty(r,t);void 0!==n&&(this._attributeToPropertyMap.set(n,r),e.push(n))})),e}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;const e=Object.getPrototypeOf(this)._classProperties;void 0!==e&&e.forEach(((e,t)=>this._classProperties.set(t,e)))}}static createProperty(e,t=q){if(this._ensureClassProperties(),this._classProperties.set(e,t),t.noAccessor||this.prototype.hasOwnProperty(e))return;const r="symbol"==typeof e?Symbol():`__${e}`,n=this.getPropertyDescriptor(e,r,t);void 0!==n&&Object.defineProperty(this.prototype,e,n)}static getPropertyDescriptor(e,t,r){return{get(){return this[t]},set(n){const a=this[e];this[t]=n,this.requestUpdateInternal(e,a,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this._classProperties&&this._classProperties.get(e)||q}static finalize(){const e=Object.getPrototypeOf(this);if(e.hasOwnProperty(U)||e.finalize(),this.finalized=!0,this._ensureClassProperties(),this._attributeToPropertyMap=new Map,this.hasOwnProperty(JSCompiler_renameProperty("properties",this))){const e=this.properties,t=[...Object.getOwnPropertyNames(e),..."function"==typeof Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e):[]];for(const r of t)this.createProperty(r,e[r])}}static _attributeNameForProperty(e,t){const r=t.attribute;return!1===r?void 0:"string"==typeof r?r:"string"==typeof e?e.toLowerCase():void 0}static _valueHasChanged(e,t,r=z){return r(e,t)}static _propertyValueFromAttribute(e,t){const r=t.type,n=t.converter||B,a="function"==typeof n?n:n.fromAttribute;return a?a(e,r):e}static _propertyValueToAttribute(e,t){if(void 0===t.reflect)return;const r=t.type,n=t.converter;return(n&&n.toAttribute||B.toAttribute)(e,r)}initialize(){this._updateState=0,this._updatePromise=new Promise((e=>this._enableUpdatingResolver=e)),this._changedProperties=new Map,this._saveInstanceProperties(),this.requestUpdateInternal()}_saveInstanceProperties(){this.constructor._classProperties.forEach(((e,t)=>{if(this.hasOwnProperty(t)){const e=this[t];delete this[t],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(t,e)}}))}_applyInstanceProperties(){this._instanceProperties.forEach(((e,t)=>this[t]=e)),this._instanceProperties=void 0}connectedCallback(){this.enableUpdating()}enableUpdating(){void 0!==this._enableUpdatingResolver&&(this._enableUpdatingResolver(),this._enableUpdatingResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(e,t,r){t!==r&&this._attributeToProperty(e,r)}_propertyToAttribute(e,t,r=q){const n=this.constructor,a=n._attributeNameForProperty(e,r);if(void 0!==a){const e=n._propertyValueToAttribute(t,r);if(void 0===e)return;this._updateState=8|this._updateState,null==e?this.removeAttribute(a):this.setAttribute(a,e),this._updateState=-9&this._updateState}}_attributeToProperty(e,t){if(8&this._updateState)return;const r=this.constructor,n=r._attributeToPropertyMap.get(e);if(void 0!==n){const e=r.getPropertyOptions(n);this._updateState=16|this._updateState,this[n]=r._propertyValueFromAttribute(t,e),this._updateState=-17&this._updateState}}requestUpdateInternal(e,t,r){let n=!0;if(void 0!==e){const a=this.constructor;r=r||a.getPropertyOptions(e),a._valueHasChanged(this[e],t,r.hasChanged)?(this._changedProperties.has(e)||this._changedProperties.set(e,t),!0!==r.reflect||16&this._updateState||(void 0===this._reflectingProperties&&(this._reflectingProperties=new Map),this._reflectingProperties.set(e,r))):n=!1}!this._hasRequestedUpdate&&n&&(this._updatePromise=this._enqueueUpdate())}requestUpdate(e,t){return this.requestUpdateInternal(e,t),this.updateComplete}async _enqueueUpdate(){this._updateState=4|this._updateState;try{await this._updatePromise}catch(e){}const e=this.performUpdate();return null!=e&&await e,!this._hasRequestedUpdate}get _hasRequestedUpdate(){return 4&this._updateState}get hasUpdated(){return 1&this._updateState}performUpdate(){if(!this._hasRequestedUpdate)return;this._instanceProperties&&this._applyInstanceProperties();let e=!1;const t=this._changedProperties;try{e=this.shouldUpdate(t),e?this.update(t):this._markUpdated()}catch(t){throw e=!1,this._markUpdated(),t}e&&(1&this._updateState||(this._updateState=1|this._updateState,this.firstUpdated(t)),this.updated(t))}_markUpdated(){this._changedProperties=new Map,this._updateState=-5&this._updateState}get updateComplete(){return this._getUpdateComplete()}_getUpdateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._updatePromise}shouldUpdate(e){return!0}update(e){void 0!==this._reflectingProperties&&this._reflectingProperties.size>0&&(this._reflectingProperties.forEach(((e,t)=>this._propertyToAttribute(t,this[t],e))),this._reflectingProperties=void 0),this._markUpdated()}updated(e){}firstUpdated(e){}}M.finalized=!0;const H=Element.prototype;H.msMatchesSelector||H.webkitMatchesSelector;const V=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,W=Symbol();class G{constructor(e,t){if(t!==W)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e}get styleSheet(){return void 0===this._styleSheet&&(V?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}const K=e=>new G(String(e),W),J=(e,...t)=>{const r=t.reduce(((t,r,n)=>t+(e=>{if(e instanceof G)return e.cssText;if("number"==typeof e)return e;throw new Error(`Value passed to 'css' function must be a 'css' function result: ${e}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(r)+e[n+1]),e[0]);return new G(r,W)};(window.litElementVersions||(window.litElementVersions=[])).push("2.5.1");const Y={};class Z extends M{static getStyles(){return this.styles}static _getUniqueStyles(){if(this.hasOwnProperty(JSCompiler_renameProperty("_styles",this)))return;const e=this.getStyles();if(Array.isArray(e)){const t=(e,r)=>e.reduceRight(((e,r)=>Array.isArray(r)?t(r,e):(e.add(r),e)),r),r=t(e,new Set),n=[];r.forEach((e=>n.unshift(e))),this._styles=n}else this._styles=void 0===e?[]:[e];this._styles=this._styles.map((e=>{if(e instanceof CSSStyleSheet&&!V){const t=Array.prototype.slice.call(e.cssRules).reduce(((e,t)=>e+t.cssText),"");return K(t)}return e}))}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow(this.constructor.shadowRootOptions)}adoptStyles(){const e=this.constructor._styles;0!==e.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?V?this.renderRoot.adoptedStyleSheets=e.map((e=>e instanceof CSSStyleSheet?e:e.styleSheet)):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(e.map((e=>e.cssText)),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(e){const t=this.render();super.update(e),t!==Y&&this.constructor.render(t,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach((e=>{const t=document.createElement("style");t.textContent=e.cssText,this.renderRoot.appendChild(t)})))}render(){return Y}}function Q(){return{baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}Z.finalized=!0,Z.render=(e,t,r)=>{let n=F.get(t);void 0===n&&(s(t,t.firstChild),F.set(t,n=new O(Object.assign({templateFactory:L},r))),n.appendInto(t)),n.setValue(e),n.commit()},Z.shadowRootOptions={mode:"open"};let X={baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1};const ee=/[&<>"']/,te=/[&<>"']/g,re=/[<>"']|&(?!#?\w+;)/,ne=/[<>"']|&(?!#?\w+;)/g,ae={"&":"&","<":"<",">":">",'"':""","'":"'"},oe=e=>ae[e];function ie(e,t){if(t){if(ee.test(e))return e.replace(te,oe)}else if(re.test(e))return e.replace(ne,oe);return e}const se=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function le(e){return e.replace(se,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const ce=/(^|[^\[])\^/g;function pe(e,t){e=e.source||e,t=t||"";const r={replace:(t,n)=>(n=(n=n.source||n).replace(ce,"$1"),e=e.replace(t,n),r),getRegex:()=>new RegExp(e,t)};return r}const ue=/[^\w:]/g,de=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function he(e,t,r){if(e){let e;try{e=decodeURIComponent(le(r)).replace(ue,"").toLowerCase()}catch(e){return null}if(0===e.indexOf("javascript:")||0===e.indexOf("vbscript:")||0===e.indexOf("data:"))return null}t&&!de.test(r)&&(r=function(e,t){fe[" "+e]||(me.test(e)?fe[" "+e]=e+"/":fe[" "+e]=we(e,"/",!0));const r=-1===(e=fe[" "+e]).indexOf(":");return"//"===t.substring(0,2)?r?t:e.replace(ye,"$1")+t:"/"===t.charAt(0)?r?t:e.replace(ge,"$1")+t:e+t}(t,r));try{r=encodeURI(r).replace(/%25/g,"%")}catch(e){return null}return r}const fe={},me=/^[^:]+:\/*[^/]*$/,ye=/^([^:]+:)[\s\S]*$/,ge=/^([^:]+:\/*[^/]*)[\s\S]*$/;const ve={exec:function(){}};function be(e){let t,r,n=1;for(;n{let n=!1,a=t;for(;--a>=0&&"\\"===r[a];)n=!n;return n?"|":" |"})).split(/ \|/);let n=0;if(r[0].trim()||r.shift(),r.length>0&&!r[r.length-1].trim()&&r.pop(),r.length>t)r.splice(t);else for(;r.length1;)1&t&&(r+=e),t>>=1,e+=e;return r+e}function $e(e,t,r,n){const a=t.href,o=t.title?ie(t.title):null,i=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){n.state.inLink=!0;const e={type:"link",raw:r,href:a,title:o,text:i,tokens:n.inlineTokens(i,[])};return n.state.inLink=!1,e}return{type:"image",raw:r,href:a,title:o,text:ie(i)}}class Ae{constructor(e){this.options=e||X}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:we(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],r=function(e,t){const r=e.match(/^(\s+)(?:```)/);if(null===r)return t;const n=r[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[r]=t;return r.length>=n.length?e.slice(n.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim():t[2],text:r}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=we(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}const r={type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:[]};return this.lexer.inline(r.text,r.tokens),r}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=t[0].replace(/^ *> ?/gm,"");return{type:"blockquote",raw:t[0],tokens:this.lexer.blockTokens(e,[]),text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let r,n,a,o,i,s,l,c,p,u,d,h,f=t[1].trim();const m=f.length>1,y={type:"list",raw:"",ordered:m,start:m?+f.slice(0,-1):"",loose:!1,items:[]};f=m?`\\d{1,9}\\${f.slice(-1)}`:`\\${f}`,this.options.pedantic&&(f=m?f:"[*+-]");const g=new RegExp(`^( {0,3}${f})((?: [^\\n]*)?(?:\\n|$))`);for(;e&&(h=!1,t=g.exec(e))&&!this.rules.block.hr.test(e);){if(r=t[0],e=e.substring(r.length),c=t[2].split("\n",1)[0],p=e.split("\n",1)[0],this.options.pedantic?(o=2,d=c.trimLeft()):(o=t[2].search(/[^ ]/),o=o>4?1:o,d=c.slice(o),o+=t[1].length),s=!1,!c&&/^ *$/.test(p)&&(r+=p+"\n",e=e.substring(p.length+1),h=!0),!h){const t=new RegExp(`^ {0,${Math.min(3,o-1)}}(?:[*+-]|\\d{1,9}[.)])`);for(;e&&(u=e.split("\n",1)[0],c=u,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),!t.test(c));){if(c.search(/[^ ]/)>=o||!c.trim())d+="\n"+c.slice(o);else{if(s)break;d+="\n"+c}s||c.trim()||(s=!0),r+=u+"\n",e=e.substring(u.length+1)}}y.loose||(l?y.loose=!0:/\n *\n *$/.test(r)&&(l=!0)),this.options.gfm&&(n=/^\[[ xX]\] /.exec(d),n&&(a="[ ] "!==n[0],d=d.replace(/^\[[ xX]\] +/,""))),y.items.push({type:"list_item",raw:r,task:!!n,checked:a,loose:!1,text:d}),y.raw+=r}y.items[y.items.length-1].raw=r.trimRight(),y.items[y.items.length-1].text=d.trimRight(),y.raw=y.raw.trimRight();const v=y.items.length;for(i=0;i"space"===e.type)),t=e.every((e=>{const t=e.raw.split("");let r=0;for(const e of t)if("\n"===e&&(r+=1),r>1)return!0;return!1}));!y.loose&&e.length&&t&&(y.loose=!0,y.items[i].loose=!0)}return y}}html(e){const t=this.rules.block.html.exec(e);if(t){const e={type:"html",raw:t[0],pre:!this.options.sanitizer&&("pre"===t[1]||"script"===t[1]||"style"===t[1]),text:t[0]};return this.options.sanitize&&(e.type="paragraph",e.text=this.options.sanitizer?this.options.sanitizer(t[0]):ie(t[0]),e.tokens=[],this.lexer.inline(e.text,e.tokens)),e}}def(e){const t=this.rules.block.def.exec(e);if(t){t[3]&&(t[3]=t[3].substring(1,t[3].length-1));return{type:"def",tag:t[1].toLowerCase().replace(/\s+/g," "),raw:t[0],href:t[2],title:t[3]}}}table(e){const t=this.rules.block.table.exec(e);if(t){const e={type:"table",header:xe(t[1]).map((e=>({text:e}))),align:t[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(e.header.length===e.align.length){e.raw=t[0];let r,n,a,o,i=e.align.length;for(r=0;r({text:e})));for(i=e.header.length,n=0;n/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(t[0]):ie(t[0]):t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=we(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;const r=e.length;let n=0,a=0;for(;a-1){const r=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,r).trim(),t[3]=""}}let r=t[2],n="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r);e&&(r=e[1],n=e[3])}else n=t[3]?t[3].slice(1,-1):"";return r=r.trim(),/^$/.test(e)?r.slice(1):r.slice(1,-1)),$e(t,{href:r?r.replace(this.rules.inline._escapes,"$1"):r,title:n?n.replace(this.rules.inline._escapes,"$1"):n},t[0],this.lexer)}}reflink(e,t){let r;if((r=this.rules.inline.reflink.exec(e))||(r=this.rules.inline.nolink.exec(e))){let e=(r[2]||r[1]).replace(/\s+/g," ");if(e=t[e.toLowerCase()],!e||!e.href){const e=r[0].charAt(0);return{type:"text",raw:e,text:e}}return $e(r,e,r[0],this.lexer)}}emStrong(e,t,r=""){let n=this.rules.inline.emStrong.lDelim.exec(e);if(!n)return;if(n[3]&&r.match(/[\p{L}\p{N}]/u))return;const a=n[1]||n[2]||"";if(!a||a&&(""===r||this.rules.inline.punctuation.exec(r))){const r=n[0].length-1;let a,o,i=r,s=0;const l="*"===n[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(l.lastIndex=0,t=t.slice(-1*e.length+r);null!=(n=l.exec(t));){if(a=n[1]||n[2]||n[3]||n[4]||n[5]||n[6],!a)continue;if(o=a.length,n[3]||n[4]){i+=o;continue}if((n[5]||n[6])&&r%3&&!((r+o)%3)){s+=o;continue}if(i-=o,i>0)continue;if(o=Math.min(o,o+i+s),Math.min(r,o)%2){const t=e.slice(1,r+n.index+o);return{type:"em",raw:e.slice(0,r+n.index+o+1),text:t,tokens:this.lexer.inlineTokens(t,[])}}const t=e.slice(2,r+n.index+o-1);return{type:"strong",raw:e.slice(0,r+n.index+o+1),text:t,tokens:this.lexer.inlineTokens(t,[])}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const r=/[^ ]/.test(e),n=/^ /.test(e)&&/ $/.test(e);return r&&n&&(e=e.substring(1,e.length-1)),e=ie(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2],[])}}autolink(e,t){const r=this.rules.inline.autolink.exec(e);if(r){let e,n;return"@"===r[2]?(e=ie(this.options.mangle?t(r[1]):r[1]),n="mailto:"+e):(e=ie(r[1]),n=e),{type:"link",raw:r[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e,t){let r;if(r=this.rules.inline.url.exec(e)){let e,n;if("@"===r[2])e=ie(this.options.mangle?t(r[0]):r[0]),n="mailto:"+e;else{let t;do{t=r[0],r[0]=this.rules.inline._backpedal.exec(r[0])[0]}while(t!==r[0]);e=ie(r[0]),n="www."===r[1]?"http://"+e:e}return{type:"link",raw:r[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e,t){const r=this.rules.inline.text.exec(e);if(r){let e;return e=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(r[0]):ie(r[0]):r[0]:ie(this.options.smartypants?t(r[0]):r[0]),{type:"text",raw:r[0],text:e}}}}const Ee={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)( [^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?]+)>?(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:ve,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};Ee.def=pe(Ee.def).replace("label",Ee._label).replace("title",Ee._title).getRegex(),Ee.bullet=/(?:[*+-]|\d{1,9}[.)])/,Ee.listItemStart=pe(/^( *)(bull) */).replace("bull",Ee.bullet).getRegex(),Ee.list=pe(Ee.list).replace(/bull/g,Ee.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+Ee.def.source+")").getRegex(),Ee._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",Ee._comment=/|$)/,Ee.html=pe(Ee.html,"i").replace("comment",Ee._comment).replace("tag",Ee._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Ee.paragraph=pe(Ee._paragraph).replace("hr",Ee.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",Ee._tag).getRegex(),Ee.blockquote=pe(Ee.blockquote).replace("paragraph",Ee.paragraph).getRegex(),Ee.normal=be({},Ee),Ee.gfm=be({},Ee.normal,{table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),Ee.gfm.table=pe(Ee.gfm.table).replace("hr",Ee.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",Ee._tag).getRegex(),Ee.gfm.paragraph=pe(Ee._paragraph).replace("hr",Ee.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",Ee.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",Ee._tag).getRegex(),Ee.pedantic=be({},Ee.normal,{html:pe("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",Ee._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:ve,paragraph:pe(Ee.normal._paragraph).replace("hr",Ee.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",Ee.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()});const Oe={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:ve,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^[^_*]*?\_\_[^_*]*?\*[^_*]*?(?=\_\_)|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/,rDelimUnd:/^[^_*]*?\*\*[^_*]*?\_[^_*]*?(?=\*\*)|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:ve,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\.5&&(r="x"+r.toString(16)),n+="&#"+r+";";return n}Oe._punctuation="!\"#$%&'()+\\-.,/:;<=>?@\\[\\]`^{|}~",Oe.punctuation=pe(Oe.punctuation).replace(/punctuation/g,Oe._punctuation).getRegex(),Oe.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,Oe.escapedEmSt=/\\\*|\\_/g,Oe._comment=pe(Ee._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),Oe.emStrong.lDelim=pe(Oe.emStrong.lDelim).replace(/punct/g,Oe._punctuation).getRegex(),Oe.emStrong.rDelimAst=pe(Oe.emStrong.rDelimAst,"g").replace(/punct/g,Oe._punctuation).getRegex(),Oe.emStrong.rDelimUnd=pe(Oe.emStrong.rDelimUnd,"g").replace(/punct/g,Oe._punctuation).getRegex(),Oe._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,Oe._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,Oe._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,Oe.autolink=pe(Oe.autolink).replace("scheme",Oe._scheme).replace("email",Oe._email).getRegex(),Oe._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,Oe.tag=pe(Oe.tag).replace("comment",Oe._comment).replace("attribute",Oe._attribute).getRegex(),Oe._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Oe._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,Oe._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,Oe.link=pe(Oe.link).replace("label",Oe._label).replace("href",Oe._href).replace("title",Oe._title).getRegex(),Oe.reflink=pe(Oe.reflink).replace("label",Oe._label).replace("ref",Ee._label).getRegex(),Oe.nolink=pe(Oe.nolink).replace("ref",Ee._label).getRegex(),Oe.reflinkSearch=pe(Oe.reflinkSearch,"g").replace("reflink",Oe.reflink).replace("nolink",Oe.nolink).getRegex(),Oe.normal=be({},Oe),Oe.pedantic=be({},Oe.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:pe(/^!?\[(label)\]\((.*?)\)/).replace("label",Oe._label).getRegex(),reflink:pe(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Oe._label).getRegex()}),Oe.gfm=be({},Oe.normal,{escape:pe(Oe.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\!!(r=n.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.space(e))e=e.substring(r.raw.length),1===r.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(r);else if(r=this.tokenizer.code(e))e=e.substring(r.raw.length),n=t[t.length-1],!n||"paragraph"!==n.type&&"text"!==n.type?t.push(r):(n.raw+="\n"+r.raw,n.text+="\n"+r.text,this.inlineQueue[this.inlineQueue.length-1].src=n.text);else if(r=this.tokenizer.fences(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.heading(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.hr(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.blockquote(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.list(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.html(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.def(e))e=e.substring(r.raw.length),n=t[t.length-1],!n||"paragraph"!==n.type&&"text"!==n.type?this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title}):(n.raw+="\n"+r.raw,n.text+="\n"+r.raw,this.inlineQueue[this.inlineQueue.length-1].src=n.text);else if(r=this.tokenizer.table(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.lheading(e))e=e.substring(r.raw.length),t.push(r);else{if(a=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const r=e.slice(1);let n;this.options.extensions.startBlock.forEach((function(e){n=e.call({lexer:this},r),"number"==typeof n&&n>=0&&(t=Math.min(t,n))})),t<1/0&&t>=0&&(a=e.substring(0,t+1))}if(this.state.top&&(r=this.tokenizer.paragraph(a)))n=t[t.length-1],o&&"paragraph"===n.type?(n.raw+="\n"+r.raw,n.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=n.text):t.push(r),o=a.length!==e.length,e=e.substring(r.raw.length);else if(r=this.tokenizer.text(e))e=e.substring(r.raw.length),n=t[t.length-1],n&&"text"===n.type?(n.raw+="\n"+r.raw,n.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=n.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t){this.inlineQueue.push({src:e,tokens:t})}inlineTokens(e,t=[]){let r,n,a,o,i,s,l=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(o=this.tokenizer.rules.inline.reflinkSearch.exec(l));)e.includes(o[0].slice(o[0].lastIndexOf("[")+1,-1))&&(l=l.slice(0,o.index)+"["+ke("a",o[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(o=this.tokenizer.rules.inline.blockSkip.exec(l));)l=l.slice(0,o.index)+"["+ke("a",o[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(o=this.tokenizer.rules.inline.escapedEmSt.exec(l));)l=l.slice(0,o.index)+"++"+l.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex);for(;e;)if(i||(s=""),i=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((n=>!!(r=n.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.escape(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.tag(e))e=e.substring(r.raw.length),n=t[t.length-1],n&&"text"===r.type&&"text"===n.type?(n.raw+=r.raw,n.text+=r.text):t.push(r);else if(r=this.tokenizer.link(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(r.raw.length),n=t[t.length-1],n&&"text"===r.type&&"text"===n.type?(n.raw+=r.raw,n.text+=r.text):t.push(r);else if(r=this.tokenizer.emStrong(e,l,s))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.codespan(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.br(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.del(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.autolink(e,_e))e=e.substring(r.raw.length),t.push(r);else if(this.state.inLink||!(r=this.tokenizer.url(e,_e))){if(a=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const r=e.slice(1);let n;this.options.extensions.startInline.forEach((function(e){n=e.call({lexer:this},r),"number"==typeof n&&n>=0&&(t=Math.min(t,n))})),t<1/0&&t>=0&&(a=e.substring(0,t+1))}if(r=this.tokenizer.inlineText(a,Te))e=e.substring(r.raw.length),"_"!==r.raw.slice(-1)&&(s=r.raw.slice(-1)),i=!0,n=t[t.length-1],n&&"text"===n.type?(n.raw+=r.raw,n.text+=r.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(r.raw.length),t.push(r);return t}}class je{constructor(e){this.options=e||X}code(e,t,r){const n=(t||"").match(/\S*/)[0];if(this.options.highlight){const t=this.options.highlight(e,n);null!=t&&t!==e&&(r=!0,e=t)}return e=e.replace(/\n$/,"")+"\n",n?'
'+(r?e:ie(e,!0))+"
\n":"
"+(r?e:ie(e,!0))+"
\n"}blockquote(e){return"
\n"+e+"
\n"}html(e){return e}heading(e,t,r,n){return this.options.headerIds?"'+e+"\n":""+e+"\n"}hr(){return this.options.xhtml?"
\n":"
\n"}list(e,t,r){const n=t?"ol":"ul";return"<"+n+(t&&1!==r?' start="'+r+'"':"")+">\n"+e+"\n"}listitem(e){return"
  • "+e+"
  • \n"}checkbox(e){return" "}paragraph(e){return"

    "+e+"

    \n"}table(e,t){return t&&(t=""+t+""),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return"\n"+e+"\n"}tablecell(e,t){const r=t.header?"th":"td";return(t.align?"<"+r+' align="'+t.align+'">':"<"+r+">")+e+"\n"}strong(e){return""+e+""}em(e){return""+e+""}codespan(e){return""+e+""}br(){return this.options.xhtml?"
    ":"
    "}del(e){return""+e+""}link(e,t,r){if(null===(e=he(this.options.sanitize,this.options.baseUrl,e)))return r;let n='",n}image(e,t,r){if(null===(e=he(this.options.sanitize,this.options.baseUrl,e)))return r;let n=''+r+'":">",n}text(e){return e}}class Ie{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,r){return""+r}image(e,t,r){return""+r}br(){return""}}class Pe{constructor(){this.seen={}}serialize(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")}getNextSafeSlug(e,t){let r=e,n=0;if(this.seen.hasOwnProperty(r)){n=this.seen[e];do{n++,r=e+"-"+n}while(this.seen.hasOwnProperty(r))}return t||(this.seen[e]=n,this.seen[r]=0),r}slug(e,t={}){const r=this.serialize(e);return this.getNextSafeSlug(r,t.dryrun)}}class Re{constructor(e){this.options=e||X,this.options.renderer=this.options.renderer||new je,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new Ie,this.slugger=new Pe}static parse(e,t){return new Re(t).parse(e)}static parseInline(e,t){return new Re(t).parseInline(e)}parse(e,t=!0){let r,n,a,o,i,s,l,c,p,u,d,h,f,m,y,g,v,b,x,w="";const S=e.length;for(r=0;r0&&"paragraph"===y.tokens[0].type?(y.tokens[0].text=b+" "+y.tokens[0].text,y.tokens[0].tokens&&y.tokens[0].tokens.length>0&&"text"===y.tokens[0].tokens[0].type&&(y.tokens[0].tokens[0].text=b+" "+y.tokens[0].tokens[0].text)):y.tokens.unshift({type:"text",text:b}):m+=b),m+=this.parse(y.tokens,f),p+=this.renderer.listitem(m,v,g);w+=this.renderer.list(p,d,h);continue;case"html":w+=this.renderer.html(u.text);continue;case"paragraph":w+=this.renderer.paragraph(this.parseInline(u.tokens));continue;case"text":for(p=u.tokens?this.parseInline(u.tokens):u.text;r+1{n(e.text,e.lang,(function(t,r){if(t)return o(t);null!=r&&r!==e.text&&(e.text=r,e.escaped=!0),i--,0===i&&o()}))}),0))})),void(0===i&&o())}try{const r=Ce.lex(e,t);return t.walkTokens&&Le.walkTokens(r,t.walkTokens),Re.parse(r,t)}catch(e){if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",t.silent)return"

    An error occurred:

    "+ie(e.message+"",!0)+"
    ";throw e}}Le.options=Le.setOptions=function(e){var t;return be(Le.defaults,e),t=Le.defaults,X=t,Le},Le.getDefaults=Q,Le.defaults=X,Le.use=function(...e){const t=be({},...e),r=Le.defaults.extensions||{renderers:{},childTokens:{}};let n;e.forEach((e=>{if(e.extensions&&(n=!0,e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if(e.renderer){const t=r.renderers?r.renderers[e.name]:null;r.renderers[e.name]=t?function(...r){let n=e.renderer.apply(this,r);return!1===n&&(n=t.apply(this,r)),n}:e.renderer}if(e.tokenizer){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");r[e.level]?r[e.level].unshift(e.tokenizer):r[e.level]=[e.tokenizer],e.start&&("block"===e.level?r.startBlock?r.startBlock.push(e.start):r.startBlock=[e.start]:"inline"===e.level&&(r.startInline?r.startInline.push(e.start):r.startInline=[e.start]))}e.childTokens&&(r.childTokens[e.name]=e.childTokens)}))),e.renderer){const r=Le.defaults.renderer||new je;for(const t in e.renderer){const n=r[t];r[t]=(...a)=>{let o=e.renderer[t].apply(r,a);return!1===o&&(o=n.apply(r,a)),o}}t.renderer=r}if(e.tokenizer){const r=Le.defaults.tokenizer||new Ae;for(const t in e.tokenizer){const n=r[t];r[t]=(...a)=>{let o=e.tokenizer[t].apply(r,a);return!1===o&&(o=n.apply(r,a)),o}}t.tokenizer=r}if(e.walkTokens){const r=Le.defaults.walkTokens;t.walkTokens=function(t){e.walkTokens.call(this,t),r&&r.call(this,t)}}n&&(t.extensions=r),Le.setOptions(t)}))},Le.walkTokens=function(e,t){for(const r of e)switch(t.call(Le,r),r.type){case"table":for(const e of r.header)Le.walkTokens(e.tokens,t);for(const e of r.rows)for(const r of e)Le.walkTokens(r.tokens,t);break;case"list":Le.walkTokens(r.items,t);break;default:Le.defaults.extensions&&Le.defaults.extensions.childTokens&&Le.defaults.extensions.childTokens[r.type]?Le.defaults.extensions.childTokens[r.type].forEach((function(e){Le.walkTokens(r[e],t)})):r.tokens&&Le.walkTokens(r.tokens,t)}},Le.parseInline=function(e,t){if(null==e)throw new Error("marked.parseInline(): input parameter is undefined or null");if("string"!=typeof e)throw new Error("marked.parseInline(): input parameter is of type "+Object.prototype.toString.call(e)+", string expected");Se(t=be({},Le.defaults,t||{}));try{const r=Ce.lexInline(e,t);return t.walkTokens&&Le.walkTokens(r,t.walkTokens),Re.parseInline(r,t)}catch(e){if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",t.silent)return"

    An error occurred:

    "+ie(e.message+"",!0)+"
    ";throw e}},Le.Parser=Re,Le.parser=Re.parse,Le.Renderer=je,Le.TextRenderer=Ie,Le.Lexer=Ce,Le.lexer=Ce.lex,Le.Tokenizer=Ae,Le.Slugger=Pe,Le.parse=Le;Le.options,Le.setOptions,Le.use,Le.walkTokens,Le.parseInline,Re.parse,Ce.lex;var Ne=r(660),Fe=r.n(Ne);r(251),r(358),r(46),r(503),r(277),r(874),r(366),r(57),r(16);const De=J`.hover-bg:hover{background:var(--bg3)}::selection{background:var(--selection-bg);color:var(--selection-fg)}.regular-font{font-family:var(--font-regular)}.mono-font{font-family:var(--font-mono)}.title{font-size:calc(var(--font-size-small) + 18px);font-weight:400}.sub-title{font-size:20px}.req-res-title{font-family:var(--font-regular);font-size:calc(var(--font-size-small) + 4px);font-weight:700;margin-bottom:8px;text-align:left}.tiny-title{font-size:calc(var(--font-size-small) + 1px);font-weight:700}.regular-font-size{font-size:var(--font-size-regular)}.small-font-size{font-size:var(--font-size-small)}.upper{text-transform:uppercase}.primary-text{color:var(--primary-color)}.bold-text{font-weight:700}.gray-text{color:var(--light-fg)}.red-text{color:var(--red)}.blue-text{color:var(--blue)}.multiline{overflow:scroll;max-height:var(--resp-area-height,300px);color:var(--fg3)}.method-fg.put{color:var(--orange)}.method-fg.post{color:var(--green)}.method-fg.get{color:var(--blue)}.method-fg.delete{color:var(--red)}.method-fg.head,.method-fg.options,.method-fg.patch{color:var(--yellow)}h1{font-family:var(--font-regular);font-size:28px;padding-top:10px;letter-spacing:normal;font-weight:400}h2{font-family:var(--font-regular);font-size:24px;padding-top:10px;letter-spacing:normal;font-weight:400}h3{font-family:var(--font-regular);font-size:18px;padding-top:10px;letter-spacing:normal;font-weight:400}h4{font-family:var(--font-regular);font-size:16px;padding-top:10px;letter-spacing:normal;font-weight:400}h5{font-family:var(--font-regular);font-size:14px;padding-top:10px;letter-spacing:normal;font-weight:400}h6{font-family:var(--font-regular);font-size:14px;padding-top:10px;letter-spacing:normal;font-weight:400}h1,h2,h3,h4,h5{margin-block-end:.2em}p{margin-block-start:.5em}a{color:var(--blue);cursor:pointer}a.inactive-link{color:var(--fg);text-decoration:none;cursor:text}code,pre{margin:0;font-family:var(--font-mono);font-size:calc(var(--font-size-mono) - 1px)}.m-markdown,.m-markdown-small{display:block}.m-markdown p,.m-markdown span{font-size:var(--font-size-regular);line-height:calc(var(--font-size-regular) + 8px)}.m-markdown li{font-size:var(--font-size-regular);line-height:calc(var(--font-size-regular) + 10px)}.m-markdown-small li,.m-markdown-small p,.m-markdown-small span{font-size:var(--font-size-small);line-height:calc(var(--font-size-small) + 6px)}.m-markdown-small li{line-height:calc(var(--font-size-small) + 8px)}.m-markdown p:not(:first-child){margin-block-start:24px}.m-markdown-small p:not(:first-child){margin-block-start:12px}.m-markdown-small p:first-child{margin-block-start:0}.m-markdown p,.m-markdown-small p{margin-block-end:0}.m-markdown code span{font-size:var(--font-size-mono)}.m-markdown code,.m-markdown-small code{padding:1px 6px;border-radius:2px;color:var(--inline-code-fg);background-color:var(--bg3);font-size:calc(var(--font-size-mono));line-height:1.2}.m-markdown-small code{font-size:calc(var(--font-size-mono) - 1px)}.m-markdown pre,.m-markdown-small pre{white-space:pre-wrap;overflow-x:auto;line-height:normal;border-radius:2px;border:1px solid var(--code-border-color)}.m-markdown pre{padding:12px;background-color:var(--code-bg);color:var(--code-fg)}.m-markdown-small pre{margin-top:4px;padding:2px 4px;background-color:var(--bg3);color:var(--fg2)}.m-markdown pre code,.m-markdown-small pre code{border:none;padding:0}.m-markdown pre code{color:var(--code-fg);background-color:var(--code-bg);background-color:transparent}.m-markdown-small pre code{color:var(--fg2);background-color:var(--bg3)}.m-markdown ol,.m-markdown ul{padding-inline-start:30px}.m-markdown-small ol,.m-markdown-small ul{padding-inline-start:20px}.m-markdown a,.m-markdown-small a{color:var(--blue)}.m-markdown img,.m-markdown-small img{max-width:100%}.m-markdown table,.m-markdown-small table{border-spacing:0;margin:10px 0;border-collapse:separate;border:1px solid var(--border-color);border-radius:var(--border-radius);font-size:calc(var(--font-size-small) + 1px);line-height:calc(var(--font-size-small) + 4px);max-width:100%}.m-markdown-small table{font-size:var(--font-size-small);line-height:calc(var(--font-size-small) + 2px);margin:8px 0}.m-markdown td,.m-markdown th,.m-markdown-small td,.m-markdown-small th{vertical-align:top;border-top:1px solid var(--border-color);line-height:calc(var(--font-size-small) + 4px)}.m-markdown tr:first-child th,.m-markdown-small tr:first-child th{border-top:0 none}.m-markdown td,.m-markdown th{padding:10px 12px}.m-markdown-small td,.m-markdown-small th{padding:8px 8px}.m-markdown th,.m-markdown-small th{font-weight:600;background-color:var(--bg2);vertical-align:middle}.m-markdown-small table code{font-size:calc(var(--font-size-mono) - 2px)}.m-markdown table code{font-size:calc(var(--font-size-mono) - 1px)}.m-markdown blockquote,.m-markdown-small blockquote{margin-inline-start:0;margin-inline-end:0;border-left:3px solid var(--border-color);padding:6px 0 6px 6px}.m-markdown hr{border:1px solid var(--border-color)}`,Be=J`.m-btn{border-radius:var(--border-radius);font-weight:600;display:inline-block;padding:6px 16px;font-size:var(--font-size-small);outline:0;line-height:1;text-align:center;white-space:nowrap;border:2px solid var(--primary-color);background-color:transparent;transition:background-color .2s;user-select:none;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)}.m-btn.primary{background-color:var(--primary-color);color:var(--primary-color-invert)}.m-btn.thin-border{border-width:1px}.m-btn.large{padding:8px 14px}.m-btn.small{padding:5px 12px}.m-btn.tiny{padding:5px 6px}.m-btn.circle{border-radius:50%}.m-btn:hover{background-color:var(--primary-color);color:var(--primary-color-invert)}.m-btn.nav{border:2px solid var(--nav-accent-color)}.m-btn.nav:hover{background-color:var(--nav-accent-color)}.m-btn:disabled{background-color:var(--bg3);color:var(--fg3);border-color:var(--fg3);cursor:not-allowed;opacity:.4}.toolbar-btn{cursor:pointer;padding:4px;margin:0 2px;font-size:var(--font-size-small);min-width:50px;color:var(--primary-color-invert);border-radius:2px;border:none;background-color:var(--primary-color)}button,input,pre,select,textarea{color:var(--fg);outline:0;background-color:var(--input-bg);border:1px solid var(--border-color);border-radius:var(--border-radius)}button{font-family:var(--font-regular)}input[type=file],input[type=password],input[type=text],pre,select,textarea{font-family:var(--font-mono);font-weight:400;font-size:var(--font-size-small);transition:border .2s;padding:6px 5px}select{font-family:var(--font-regular);padding:5px 30px 5px 5px;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%3E%3Cpath%20d%3D%22M10.3%203.3L6%207.6%201.7%203.3A1%201%200%2000.3%204.7l5%205a1%201%200%20001.4%200l5-5a1%201%200%2010-1.4-1.4z%22%20fill%3D%22%23777777%22%2F%3E%3C%2Fsvg%3E");background-position:calc(100% - 5px) center;background-repeat:no-repeat;background-size:10px;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer}select:hover{border-color:var(--primary-color)}input[type=password]::placeholder,input[type=text]::placeholder,textarea::placeholder{color:var(--placeholder-color);opacity:1}input[type=password]:active,input[type=password]:focus,input[type=text]:active,input[type=text]:focus,select:focus,textarea:active,textarea:focus{border:1px solid var(--primary-color)}input[type=file]{font-family:var(--font-regular);padding:2px;cursor:pointer;border:1px solid var(--primary-color);min-height:calc(var(--font-size-small) + 18px)}input[type=file]::-webkit-file-upload-button{font-family:var(--font-regular);font-size:var(--font-size-small);outline:0;cursor:pointer;padding:3px 8px;border:1px solid var(--primary-color);background-color:var(--primary-color);color:var(--primary-color-invert);border-radius:var(--border-radius);-webkit-appearance:none}pre,textarea{scrollbar-width:thin;scrollbar-color:var(--border-color) var(--input-bg)}pre::-webkit-scrollbar,textarea::-webkit-scrollbar{width:8px;height:8px}pre::-webkit-scrollbar-track,textarea::-webkit-scrollbar-track{background:var(--input-bg)}pre::-webkit-scrollbar-thumb,textarea::-webkit-scrollbar-thumb{border-radius:2px;background-color:var(--border-color)}.link{font-size:var(--font-size-small);text-decoration:underline;color:var(--blue);font-family:var(--font-mono);margin-bottom:2px}input[type=checkbox]:focus{outline:0}input[type=checkbox]{appearance:none;display:inline-block;background-color:var(--light-bg);border:1px solid var(--light-bg);border-radius:9px;cursor:pointer;height:18px;position:relative;transition:border .25s .15s,box-shadow .25s .3s,padding .25s;min-width:36px;width:36px;vertical-align:top}input[type=checkbox]:after{position:absolute;background-color:var(--bg);border:1px solid var(--light-bg);border-radius:8px;content:'';top:0;left:0;right:16px;display:block;height:16px;transition:border .25s .15s,left .25s .1s,right .15s .175s}input[type=checkbox]:checked{box-shadow:inset 0 0 0 13px var(--green);border-color:var(--green)}input[type=checkbox]:checked:after{border:1px solid var(--green);left:16px;right:1px;transition:border .25s,left .15s .25s,right .25s .175s}`,ze=J`.col,.row{display:flex}.row{align-items:center;flex-direction:row}.col{align-items:stretch;flex-direction:column}`,qe=J`.m-table{border-spacing:0;border-collapse:separate;border:1px solid var(--light-border-color);border-radius:var(--border-radius);margin:0;max-width:100%;direction:ltr}.m-table tr:first-child td,.m-table tr:first-child th{border-top:0 none}.m-table td,.m-table th{font-size:var(--font-size-small);line-height:calc(var(--font-size-small) + 4px);padding:4px 5px 4px;vertical-align:top}.m-table.padded-12 td,.m-table.padded-12 th{padding:12px}.m-table td:not([align]),.m-table th:not([align]){text-align:left}.m-table th{color:var(--fg2);font-size:var(--font-size-small);line-height:calc(var(--font-size-small) + 18px);font-weight:600;letter-spacing:normal;background-color:var(--bg2);vertical-align:bottom;border-bottom:1px solid var(--light-border-color)}.m-table>tbody>tr>td,.m-table>tr>td{border-top:1px solid var(--light-border-color);text-overflow:ellipsis;overflow:hidden}.table-title{font-size:var(--font-size-small);font-weight:700;vertical-align:middle;margin:12px 0 4px 0}`,Ue=J`.only-large-screen{display:none}.endpoint-head .path{display:flex;font-family:var(--font-mono);font-size:var(--font-size-small);align-items:center;overflow-wrap:break-word;word-break:break-all}.endpoint-head .descr{font-size:var(--font-size-small);color:var(--light-fg);font-weight:400;align-items:center;overflow-wrap:break-word;word-break:break-all;display:none}.m-endpoint.expanded{margin-bottom:16px}.m-endpoint>.endpoint-head{border-width:1px 1px 1px 5px;border-style:solid;border-color:transparent;border-top-color:var(--light-border-color);display:flex;padding:6px 16px;align-items:center;cursor:pointer}.m-endpoint>.endpoint-head.put.expanded,.m-endpoint>.endpoint-head.put:hover{border-color:var(--orange);background-color:var(--light-orange)}.m-endpoint>.endpoint-head.post.expanded,.m-endpoint>.endpoint-head.post:hover{border-color:var(--green);background-color:var(--light-green)}.m-endpoint>.endpoint-head.get.expanded,.m-endpoint>.endpoint-head.get:hover{border-color:var(--blue);background-color:var(--light-blue)}.m-endpoint>.endpoint-head.delete.expanded,.m-endpoint>.endpoint-head.delete:hover{border-color:var(--red);background-color:var(--light-red)}.m-endpoint>.endpoint-head.head.expanded,.m-endpoint>.endpoint-head.head:hover,.m-endpoint>.endpoint-head.options.expanded,.m-endpoint>.endpoint-head.options:hover,.m-endpoint>.endpoint-head.patch.expanded,.m-endpoint>.endpoint-head.patch:hover{border-color:var(--yellow);background-color:var(--light-yellow)}.m-endpoint>.endpoint-head.deprecated.expanded,.m-endpoint>.endpoint-head.deprecated:hover{border-color:var(--border-color);filter:opacity(.6)}.m-endpoint .endpoint-body{flex-wrap:wrap;padding:16px 0 0 0;border-width:0 1px 1px 5px;border-style:solid;box-shadow:0 4px 3px -3px rgba(0,0,0,.15)}.m-endpoint .endpoint-body.delete{border-color:var(--red)}.m-endpoint .endpoint-body.put{border-color:var(--orange)}.m-endpoint .endpoint-body.post{border-color:var(--green)}.m-endpoint .endpoint-body.get{border-color:var(--blue)}.m-endpoint .endpoint-body.head,.m-endpoint .endpoint-body.options,.m-endpoint .endpoint-body.patch{border-color:var(--yellow)}.m-endpoint .endpoint-body.deprecated{border-color:var(--border-color);filter:opacity(.6)}.endpoint-head .deprecated{color:var(--light-fg);filter:opacity(.6)}.summary{padding:8px 8px}.summary .title{font-size:calc(var(--font-size-regular) + 2px);margin-bottom:6px;word-break:break-all}.method{padding:2px 5px;vertical-align:middle;font-size:var(--font-size-small);height:calc(var(--font-size-small) + 16px);line-height:calc(var(--font-size-small) + 8px);width:60px;border-radius:2px;display:inline-block;text-align:center;font-weight:700;text-transform:uppercase;margin-right:5px}.method.delete{border:2px solid var(--red)}.method.put{border:2px solid var(--orange)}.method.post{border:2px solid var(--green)}.method.get{border:2px solid var(--blue)}.method.get.deprecated{border:2px solid var(--border-color)}.method.head,.method.options,.method.patch{border:2px solid var(--yellow)}.req-resp-container{display:flex;margin-top:16px;align-items:stretch;flex-wrap:wrap;flex-direction:column;border-top:1px solid var(--light-border-color)}.view-mode-request,api-response.view-mode{flex:1;min-height:100px;padding:16px 8px;overflow:hidden}.view-mode-request{border-width:0 0 1px 0;border-style:dashed}.head .view-mode-request,.options .view-mode-request,.patch .view-mode-request{border-color:var(--yellow)}.put .view-mode-request{border-color:var(--orange)}.post .view-mode-request{border-color:var(--green)}.get .view-mode-request{border-color:var(--blue)}.delete .view-mode-request{border-color:var(--red)}@media only screen and (min-width:1024px){.only-large-screen{display:block}.endpoint-head .path{font-size:var(--font-size-regular)}.endpoint-head .descr{display:flex}.descr .m-markdown-small,.endpoint-head .m-markdown-small{display:block}.req-resp-container{flex-direction:var(--layout,row);flex-wrap:nowrap}api-response.view-mode{padding:16px}.view-mode-request.row-layout{border-width:0 1px 0 0;padding:16px}.summary{padding:8px 16px}}`,Me=J`code[class*=language-],pre[class*=language-]{text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;tab-size:2;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-]{white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:var(--light-fg)}.token.punctuation{color:var(--fg)}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:var(--pink)}.token.function-name{color:var(--blue)}.token.boolean,.token.function,.token.number{color:var(--red)}.token.class-name,.token.constant,.token.property,.token.symbol{color:var(--code-property-color)}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:var(--code-keyword-color)}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:var(--green)}.token.entity,.token.operator,.token.url{color:var(--code-operator-color)}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}`,He=J`.tab-panel{border:none}.tab-buttons{height:30px;border-bottom:1px solid var(--light-border-color);align-items:stretch;overflow-y:hidden;overflow-x:auto;scrollbar-width:thin}.tab-buttons::-webkit-scrollbar{height:1px;background-color:var(--border-color)}.tab-btn{border:none;border-bottom:3px solid transparent;color:var(--light-fg);background-color:transparent;white-space:nowrap;cursor:pointer;outline:0;font-family:var(--font-regular);font-size:var(--font-size-small);margin-right:16px;padding:1px}.tab-btn.active{border-bottom:3px solid var(--primary-color);font-weight:700;color:var(--primary-color)}.tab-btn:hover{color:var(--primary-color)}.tab-content{margin:-1px 0 0 0;position:relative}`,Ve=J`.nav-bar{width:0;height:100%;overflow:hidden;color:var(--nav-text-color);background-color:var(--nav-bg-color);background-blend-mode:multiply;line-height:calc(var(--font-size-small) + 4px);display:none;position:relative;flex-direction:column;flex-wrap:nowrap;word-break:break-word}::slotted([slot=nav-logo]){padding:16px 16px 0 16px}.nav-scroll{overflow-x:hidden;overflow-y:auto;overflow-y:overlay;scrollbar-width:thin;scrollbar-color:var(--nav-hover-bg-color) transparent}.nav-bar-tag{display:flex;align-items:center;justify-content:space-between;flex-direction:row}.nav-bar.read .nav-bar-tag-icon{display:none}.nav-bar-tag-icon{color:var(--nav-text-color);font-size:20px}.nav-bar-tag-icon:hover{color:var(--nav-hover-text-color)}.nav-bar.focused .nav-bar-tag-and-paths.collapsed .nav-bar-paths-under-tag{display:none}.nav-bar.focused .nav-bar-tag-and-paths.collapsed .nav-bar-tag-icon::after{content:'⌵';width:16px;height:16px;text-align:center;display:inline-block;transform:rotate(270deg)}.nav-bar.focused .nav-bar-tag-and-paths.expanded .nav-bar-tag-icon::after{content:'⌵';width:16px;height:16px;text-align:center;display:inline-block}.nav-scroll::-webkit-scrollbar{width:var(--scroll-bar-width,8px)}.nav-scroll::-webkit-scrollbar-track{background:0 0}.nav-scroll::-webkit-scrollbar-thumb{background-color:var(--nav-hover-bg-color)}.nav-bar-tag{font-size:var(--font-size-regular);color:var(--nav-accent-color);border-left:4px solid transparent;font-weight:700;padding:15px 15px 15px 10px;text-transform:capitalize}.nav-bar-components,.nav-bar-h1,.nav-bar-h2,.nav-bar-info,.nav-bar-path,.nav-bar-tag{display:flex;cursor:pointer;border-left:4px solid transparent}.nav-bar-h1,.nav-bar-h2,.nav-bar-path{font-size:calc(var(--font-size-small) + 1px);padding:var(--nav-item-padding)}.nav-bar-path.small-font{font-size:var(--font-size-small)}.nav-bar-info{font-size:var(--font-size-regular);padding:16px 10px;font-weight:700}.nav-bar-section{display:flex;flex-direction:row;justify-content:space-between;font-size:var(--font-size-small);color:var(--nav-text-color);padding:var(--nav-item-padding);font-weight:700}.nav-bar-section.operations{cursor:pointer}.nav-bar-section.operations:hover{color:var(--nav-hover-text-color);background-color:var(--nav-hover-bg-color)}.nav-bar-section:first-child{display:none}.nav-bar-h2{margin-left:12px}.nav-bar-h1.active,.nav-bar-h2.active,.nav-bar-info.active,.nav-bar-path.active,.nav-bar-section.operations.active,.nav-bar-tag.active{border-left:4px solid var(--nav-accent-color);color:var(--nav-hover-text-color)}.nav-bar-h1:hover,.nav-bar-h2:hover,.nav-bar-info:hover,.nav-bar-path:hover,.nav-bar-tag:hover{color:var(--nav-hover-text-color);background-color:var(--nav-hover-bg-color)}`,We=J`#api-info{font-size:calc(var(--font-size-regular) - 1px);margin-top:8px margin-left: -15px}#api-info span:before{content:"|";display:inline-block;opacity:.5;width:15px;text-align:center}#api-info span:first-child:before{content:"";width:0}`,Ge=J``;const Ke=/[\s#:?&={}]/g,Je="_rapidoc_api_key";function Ye(e){return new Promise((t=>setTimeout(t,e)))}function Ze(e,t){const r=t.currentTarget,n=document.createElement("textarea");n.value=e,n.style.position="fixed",document.body.appendChild(n),n.focus(),n.select();try{document.execCommand("copy"),r.innerText="Copied",setTimeout((()=>{r.innerText="Copy"}),5e3)}catch(e){console.error("Unable to copy",e)}document.body.removeChild(n)}function Qe(e,t,r="includes"){if("includes"===r){return`${t.method} ${t.path} ${t.summary||t.description||""} ${t.operationId||""}`.toLowerCase().includes(e.toLowerCase())}return new RegExp(e,"i").test(`${t.method} ${t.path}`)}function Xe(e,t=new Set){return e?(Object.keys(e).forEach((r=>{var n;if(t.add(r),e[r].properties)Xe(e[r].properties,t);else if(null!==(n=e[r].items)&&void 0!==n&&n.properties){var a;Xe(null===(a=e[r].items)||void 0===a?void 0:a.properties,t)}})),t):t}function et(e,t){if(e){const r=document.createElement("a");document.body.appendChild(r),r.style="display: none",r.href=e,r.download=t,r.click(),r.remove()}}function tt(e){if(e){const t=document.createElement("a");document.body.appendChild(t),t.style="display: none",t.href=e,t.target="_blank",t.click(),t.remove()}}var rt=r(764).Buffer;function nt(e){if(e.__esModule)return e;var t=Object.defineProperty({},"__esModule",{value:!0});return Object.keys(e).forEach((function(r){var n=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(t,r,n.get?n:{enumerable:!0,get:function(){return e[r]}})})),t}var at=function(e){return e&&e.Math==Math&&e},ot=at("object"==typeof globalThis&&globalThis)||at("object"==typeof window&&window)||at("object"==typeof self&&self)||at("object"==typeof ot&&ot)||function(){return this}()||Function("return this")(),it=function(e){try{return!!e()}catch(e){return!0}},st=!it((function(){var e=function(){}.bind();return"function"!=typeof e||e.hasOwnProperty("prototype")})),lt=st,ct=Function.prototype,pt=ct.apply,ut=ct.call,dt="object"==typeof Reflect&&Reflect.apply||(lt?ut.bind(pt):function(){return ut.apply(pt,arguments)}),ht=st,ft=Function.prototype,mt=ft.bind,yt=ft.call,gt=ht&&mt.bind(yt,yt),vt=ht?function(e){return e&>(e)}:function(e){return e&&function(){return yt.apply(e,arguments)}},bt=function(e){return"function"==typeof e},xt={},wt=!it((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]})),St=st,kt=Function.prototype.call,$t=St?kt.bind(kt):function(){return kt.apply(kt,arguments)},At={},Et={}.propertyIsEnumerable,Ot=Object.getOwnPropertyDescriptor,Tt=Ot&&!Et.call({1:2},1);At.f=Tt?function(e){var t=Ot(this,e);return!!t&&t.enumerable}:Et;var _t,Ct,jt=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}},It=vt,Pt=It({}.toString),Rt=It("".slice),Lt=function(e){return Rt(Pt(e),8,-1)},Nt=vt,Ft=it,Dt=Lt,Bt=ot.Object,zt=Nt("".split),qt=Ft((function(){return!Bt("z").propertyIsEnumerable(0)}))?function(e){return"String"==Dt(e)?zt(e,""):Bt(e)}:Bt,Ut=ot.TypeError,Mt=function(e){if(null==e)throw Ut("Can't call method on "+e);return e},Ht=qt,Vt=Mt,Wt=function(e){return Ht(Vt(e))},Gt=bt,Kt=function(e){return"object"==typeof e?null!==e:Gt(e)},Jt={},Yt=Jt,Zt=ot,Qt=bt,Xt=function(e){return Qt(e)?e:void 0},er=function(e,t){return arguments.length<2?Xt(Yt[e])||Xt(Zt[e]):Yt[e]&&Yt[e][t]||Zt[e]&&Zt[e][t]},tr=vt({}.isPrototypeOf),rr=er("navigator","userAgent")||"",nr=ot,ar=rr,or=nr.process,ir=nr.Deno,sr=or&&or.versions||ir&&ir.version,lr=sr&&sr.v8;lr&&(Ct=(_t=lr.split("."))[0]>0&&_t[0]<4?1:+(_t[0]+_t[1])),!Ct&&ar&&(!(_t=ar.match(/Edge\/(\d+)/))||_t[1]>=74)&&(_t=ar.match(/Chrome\/(\d+)/))&&(Ct=+_t[1]);var cr=Ct,pr=cr,ur=it,dr=!!Object.getOwnPropertySymbols&&!ur((function(){var e=Symbol();return!String(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&pr&&pr<41})),hr=dr&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,fr=er,mr=bt,yr=tr,gr=hr,vr=ot.Object,br=gr?function(e){return"symbol"==typeof e}:function(e){var t=fr("Symbol");return mr(t)&&yr(t.prototype,vr(e))},xr=ot.String,wr=function(e){try{return xr(e)}catch(e){return"Object"}},Sr=bt,kr=wr,$r=ot.TypeError,Ar=function(e){if(Sr(e))return e;throw $r(kr(e)+" is not a function")},Er=Ar,Or=function(e,t){var r=e[t];return null==r?void 0:Er(r)},Tr=$t,_r=bt,Cr=Kt,jr=ot.TypeError,Ir={exports:{}},Pr=ot,Rr=Object.defineProperty,Lr=function(e,t){try{Rr(Pr,e,{value:t,configurable:!0,writable:!0})}catch(r){Pr[e]=t}return t},Nr="__core-js_shared__",Fr=ot[Nr]||Lr(Nr,{}),Dr=Fr;(Ir.exports=function(e,t){return Dr[e]||(Dr[e]=void 0!==t?t:{})})("versions",[]).push({version:"3.21.1",mode:"pure",copyright:"© 2014-2022 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.21.1/LICENSE",source:"https://github.com/zloirock/core-js"});var Br=Mt,zr=ot.Object,qr=function(e){return zr(Br(e))},Ur=qr,Mr=vt({}.hasOwnProperty),Hr=Object.hasOwn||function(e,t){return Mr(Ur(e),t)},Vr=vt,Wr=0,Gr=Math.random(),Kr=Vr(1..toString),Jr=function(e){return"Symbol("+(void 0===e?"":e)+")_"+Kr(++Wr+Gr,36)},Yr=ot,Zr=Ir.exports,Qr=Hr,Xr=Jr,en=dr,tn=hr,rn=Zr("wks"),nn=Yr.Symbol,an=nn&&nn.for,on=tn?nn:nn&&nn.withoutSetter||Xr,sn=function(e){if(!Qr(rn,e)||!en&&"string"!=typeof rn[e]){var t="Symbol."+e;en&&Qr(nn,e)?rn[e]=nn[e]:rn[e]=tn&&an?an(t):on(t)}return rn[e]},ln=$t,cn=Kt,pn=br,un=Or,dn=function(e,t){var r,n;if("string"===t&&_r(r=e.toString)&&!Cr(n=Tr(r,e)))return n;if(_r(r=e.valueOf)&&!Cr(n=Tr(r,e)))return n;if("string"!==t&&_r(r=e.toString)&&!Cr(n=Tr(r,e)))return n;throw jr("Can't convert object to primitive value")},hn=sn,fn=ot.TypeError,mn=hn("toPrimitive"),yn=function(e,t){if(!cn(e)||pn(e))return e;var r,n=un(e,mn);if(n){if(void 0===t&&(t="default"),r=ln(n,e,t),!cn(r)||pn(r))return r;throw fn("Can't convert object to primitive value")}return void 0===t&&(t="number"),dn(e,t)},gn=br,vn=function(e){var t=yn(e,"string");return gn(t)?t:t+""},bn=Kt,xn=ot.document,wn=bn(xn)&&bn(xn.createElement),Sn=function(e){return wn?xn.createElement(e):{}},kn=Sn,$n=!wt&&!it((function(){return 7!=Object.defineProperty(kn("div"),"a",{get:function(){return 7}}).a})),An=wt,En=$t,On=At,Tn=jt,_n=Wt,Cn=vn,jn=Hr,In=$n,Pn=Object.getOwnPropertyDescriptor;xt.f=An?Pn:function(e,t){if(e=_n(e),t=Cn(t),In)try{return Pn(e,t)}catch(e){}if(jn(e,t))return Tn(!En(On.f,e,t),e[t])};var Rn=it,Ln=bt,Nn=/#|\.prototype\./,Fn=function(e,t){var r=Bn[Dn(e)];return r==qn||r!=zn&&(Ln(t)?Rn(t):!!t)},Dn=Fn.normalize=function(e){return String(e).replace(Nn,".").toLowerCase()},Bn=Fn.data={},zn=Fn.NATIVE="N",qn=Fn.POLYFILL="P",Un=Fn,Mn=Ar,Hn=st,Vn=vt(vt.bind),Wn=function(e,t){return Mn(e),void 0===t?e:Hn?Vn(e,t):function(){return e.apply(t,arguments)}},Gn={},Kn=wt&&it((function(){return 42!=Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype})),Jn=ot,Yn=Kt,Zn=Jn.String,Qn=Jn.TypeError,Xn=function(e){if(Yn(e))return e;throw Qn(Zn(e)+" is not an object")},ea=wt,ta=$n,ra=Kn,na=Xn,aa=vn,oa=ot.TypeError,ia=Object.defineProperty,sa=Object.getOwnPropertyDescriptor,la="enumerable",ca="configurable",pa="writable";Gn.f=ea?ra?function(e,t,r){if(na(e),t=aa(t),na(r),"function"==typeof e&&"prototype"===t&&"value"in r&&pa in r&&!r.writable){var n=sa(e,t);n&&n.writable&&(e[t]=r.value,r={configurable:ca in r?r.configurable:n.configurable,enumerable:la in r?r.enumerable:n.enumerable,writable:!1})}return ia(e,t,r)}:ia:function(e,t,r){if(na(e),t=aa(t),na(r),ta)try{return ia(e,t,r)}catch(e){}if("get"in r||"set"in r)throw oa("Accessors not supported");return"value"in r&&(e[t]=r.value),e};var ua=Gn,da=jt,ha=wt?function(e,t,r){return ua.f(e,t,da(1,r))}:function(e,t,r){return e[t]=r,e},fa=ot,ma=dt,ya=vt,ga=bt,va=xt.f,ba=Un,xa=Jt,wa=Wn,Sa=ha,ka=Hr,$a=function(e){var t=function(r,n,a){if(this instanceof t){switch(arguments.length){case 0:return new e;case 1:return new e(r);case 2:return new e(r,n)}return new e(r,n,a)}return ma(e,this,arguments)};return t.prototype=e.prototype,t},Aa=function(e,t){var r,n,a,o,i,s,l,c,p=e.target,u=e.global,d=e.stat,h=e.proto,f=u?fa:d?fa[p]:(fa[p]||{}).prototype,m=u?xa:xa[p]||Sa(xa,p,{})[p],y=m.prototype;for(a in t)r=!ba(u?a:p+(d?".":"#")+a,e.forced)&&f&&ka(f,a),i=m[a],r&&(s=e.noTargetGet?(c=va(f,a))&&c.value:f[a]),o=r&&s?s:t[a],r&&typeof i==typeof o||(l=e.bind&&r?wa(o,fa):e.wrap&&r?$a(o):h&&ga(o)?ya(o):o,(e.sham||o&&o.sham||i&&i.sham)&&Sa(l,"sham",!0),Sa(m,a,l),h&&(ka(xa,n=p+"Prototype")||Sa(xa,n,{}),Sa(xa[n],a,o),e.real&&y&&!y[a]&&Sa(y,a,o)))},Ea=Math.ceil,Oa=Math.floor,Ta=function(e){var t=+e;return t!=t||0===t?0:(t>0?Oa:Ea)(t)},_a=Ta,Ca=Math.max,ja=Math.min,Ia=function(e,t){var r=_a(e);return r<0?Ca(r+t,0):ja(r,t)},Pa=Ta,Ra=Math.min,La=function(e){return e>0?Ra(Pa(e),9007199254740991):0},Na=La,Fa=function(e){return Na(e.length)},Da=Wt,Ba=Ia,za=Fa,qa=function(e){return function(t,r,n){var a,o=Da(t),i=za(o),s=Ba(n,i);if(e&&r!=r){for(;i>s;)if((a=o[s++])!=a)return!0}else for(;i>s;s++)if((e||s in o)&&o[s]===r)return e||s||0;return!e&&-1}},Ua={includes:qa(!0),indexOf:qa(!1)},Ma={},Ha=Hr,Va=Wt,Wa=Ua.indexOf,Ga=Ma,Ka=vt([].push),Ja=function(e,t){var r,n=Va(e),a=0,o=[];for(r in n)!Ha(Ga,r)&&Ha(n,r)&&Ka(o,r);for(;t.length>a;)Ha(n,r=t[a++])&&(~Wa(o,r)||Ka(o,r));return o},Ya=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],Za=Ja,Qa=Ya,Xa=Object.keys||function(e){return Za(e,Qa)},eo=qr,to=Xa;Aa({target:"Object",stat:!0,forced:it((function(){to(1)}))},{keys:function(e){return to(eo(e))}});var ro=Jt.Object.keys,no=ro,ao=Lt,oo=Array.isArray||function(e){return"Array"==ao(e)},io={};io[sn("toStringTag")]="z";var so="[object z]"===String(io),lo=ot,co=so,po=bt,uo=Lt,ho=sn("toStringTag"),fo=lo.Object,mo="Arguments"==uo(function(){return arguments}()),yo=co?uo:function(e){var t,r,n;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(r=function(e,t){try{return e[t]}catch(e){}}(t=fo(e),ho))?r:mo?uo(t):"Object"==(n=uo(t))&&po(t.callee)?"Arguments":n},go=yo,vo=ot.String,bo=function(e){if("Symbol"===go(e))throw TypeError("Cannot convert a Symbol value to a string");return vo(e)},xo={},wo=wt,So=Kn,ko=Gn,$o=Xn,Ao=Wt,Eo=Xa;xo.f=wo&&!So?Object.defineProperties:function(e,t){$o(e);for(var r,n=Ao(t),a=Eo(t),o=a.length,i=0;o>i;)ko.f(e,r=a[i++],n[r]);return e};var Oo,To=er("document","documentElement"),_o=Ir.exports,Co=Jr,jo=_o("keys"),Io=function(e){return jo[e]||(jo[e]=Co(e))},Po=Xn,Ro=xo,Lo=Ya,No=Ma,Fo=To,Do=Sn,Bo=Io("IE_PROTO"),zo=function(){},qo=function(e){return"