mirror of https://github.com/h44z/wg-portal.git
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
This commit is contained in:
parent
ad267ed0a8
commit
d596f578f6
185
README.md
185
README.md
|
|
@ -38,7 +38,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL or Post
|
||||||
* Peer Expiry Feature
|
* Peer Expiry Feature
|
||||||
* Handle route and DNS settings like wg-quick does
|
* Handle route and DNS settings like wg-quick does
|
||||||
* Exposes Prometheus [metrics](#metrics)
|
* Exposes Prometheus [metrics](#metrics)
|
||||||
* ~~REST API for management and client deployment~~ (coming soon)
|
* REST API for management and client deployment
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -55,97 +55,98 @@ By default, WireGuard Portal uses a SQLite database. The database is stored in *
|
||||||
### Configuration Options
|
### Configuration Options
|
||||||
The following configuration options are available:
|
The following configuration options are available:
|
||||||
|
|
||||||
| configuration key | parent key | default_value | description |
|
| configuration key | parent key | default_value | description |
|
||||||
|----------------------------------|------------|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------------------------|------------|--------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. |
|
| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. |
|
||||||
| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
||||||
| editable_keys | core | true | Allow to edit key-pairs in the UI. |
|
| editable_keys | core | true | Allow to edit key-pairs in the UI. |
|
||||||
| create_default_peer | core | false | If an LDAP user logs in for the first time and has no peers associated, a new WireGuard peer will be created for all server interfaces. |
|
| create_default_peer | core | false | If an LDAP user logs in for the first time and has no peers associated, a new WireGuard peer will be created for all server interfaces. |
|
||||||
| create_default_peer_on_creation | core | false | If an LDAP user is created (e.g. through LDAP sync), a new WireGuard peer will be created for all server interfaces. |
|
| create_default_peer_on_creation | core | false | If an LDAP user is created (e.g. through LDAP sync), a new WireGuard peer will be created for all server interfaces. |
|
||||||
| re_enable_peer_after_user_enable | core | true | Re-enable all peers that were previously disabled due to a user disable action. |
|
| re_enable_peer_after_user_enable | core | true | Re-enable all peers that were previously disabled due to a user disable action. |
|
||||||
| delete_peer_after_user_deleted | core | false | Delete all linked peers if a user gets disabled. Otherwise the peers only get disabled. |
|
| delete_peer_after_user_deleted | core | false | Delete all linked peers if a user gets disabled. Otherwise the peers only get disabled. |
|
||||||
| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. |
|
| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. |
|
||||||
| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. |
|
| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. |
|
||||||
| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. |
|
| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. |
|
||||||
| log_level | advanced | info | The loglevel, can be one of: trace, debug, info, warn, error. |
|
| log_level | advanced | info | The loglevel, can be one of: trace, debug, info, warn, error. |
|
||||||
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
|
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
|
||||||
| log_json | advanced | false | Logs in JSON format. |
|
| log_json | advanced | false | Logs in JSON format. |
|
||||||
| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. |
|
| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. |
|
||||||
| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. |
|
| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. |
|
||||||
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. |
|
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. |
|
||||||
| use_ip_v6 | advanced | true | Enable IPv6 support. |
|
| use_ip_v6 | advanced | true | Enable IPv6 support. |
|
||||||
| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. |
|
| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. |
|
||||||
| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. |
|
| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. |
|
||||||
| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. |
|
| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. |
|
||||||
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
|
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
|
||||||
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
|
| api_admin_only | advanced | true | This flag specifies if the public REST API is available to administrators only. The API Swagger documentation is available under /api/v1/doc.html |
|
||||||
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
|
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
|
||||||
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
|
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
|
||||||
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
|
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
|
||||||
| data_collection_interval | statistics | 1m | The interval between the data collection cycles. |
|
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
|
||||||
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
|
| data_collection_interval | statistics | 1m | The interval between the data collection cycles. |
|
||||||
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
|
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
|
||||||
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
|
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
|
||||||
| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. |
|
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
|
||||||
| host | mail | 127.0.0.1 | The mail-server address. |
|
| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. |
|
||||||
| port | mail | 25 | The mail-server SMTP port. |
|
| host | mail | 127.0.0.1 | The mail-server address. |
|
||||||
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
|
| port | mail | 25 | The mail-server SMTP port. |
|
||||||
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
|
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
|
||||||
| username | mail | | The SMTP user name. |
|
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
|
||||||
| password | mail | | The SMTP password. |
|
| username | mail | | The SMTP user name. |
|
||||||
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
|
| password | mail | | The SMTP password. |
|
||||||
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
|
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
|
||||||
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
|
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
|
||||||
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
|
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
|
||||||
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
|
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
|
||||||
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
|
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
|
||||||
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
|
||||||
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
|
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
||||||
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
|
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
|
||||||
| client_id | auth/oidc | | The OAuth client id. |
|
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
|
||||||
| client_secret | auth/oidc | | The OAuth client secret. |
|
| client_id | auth/oidc | | The OAuth client id. |
|
||||||
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
|
| client_secret | auth/oidc | | The OAuth client secret. |
|
||||||
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
|
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
|
||||||
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
|
||||||
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||||
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
|
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
||||||
| client_id | auth/oauth | | The OAuth client id. |
|
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
|
||||||
| client_secret | auth/oauth | | The OAuth client secret. |
|
| client_id | auth/oauth | | The OAuth client id. |
|
||||||
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
|
| client_secret | auth/oauth | | The OAuth client secret. |
|
||||||
| token_url | auth/oauth | | The URL for the token endpoint. |
|
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
|
||||||
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
|
| token_url | auth/oauth | | The URL for the token endpoint. |
|
||||||
| scopes | auth/oauth | | OAuth scopes. |
|
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
|
||||||
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
|
| scopes | auth/oauth | | OAuth scopes. |
|
||||||
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
|
||||||
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
|
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||||
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
|
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
|
||||||
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
|
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
|
||||||
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
|
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
|
||||||
| tls_key_path | auth/ldap | | A path to the TLS key. |
|
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
|
||||||
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
|
| tls_key_path | auth/ldap | | A path to the TLS key. |
|
||||||
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
|
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
|
||||||
| bind_pass | auth/ldap | | The bind password. |
|
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
|
||||||
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
|
| bind_pass | auth/ldap | | The bind password. |
|
||||||
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
|
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
|
||||||
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
|
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
|
||||||
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
|
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
|
||||||
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
|
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
|
||||||
| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. |
|
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
|
||||||
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
| sync_interval | auth/ldap | | The time interval after which users will be synchronized from LDAP. Empty value or `0` disables synchronization. |
|
||||||
| debug | database | false | Debug database statements (log each statement). |
|
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||||
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
|
| debug | database | false | Debug database statements (log each statement). |
|
||||||
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
|
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
|
||||||
| 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 |
|
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
|
||||||
| request_logging | web | false | Log all HTTP requests. |
|
| 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 |
|
||||||
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
|
| request_logging | web | false | Log all HTTP requests. |
|
||||||
| listening_address | web | :8888 | The listening port of the web server. |
|
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
|
||||||
| session_identifier | web | wgPortalSession | The session identifier for the web frontend. |
|
| listening_address | web | :8888 | The listening port of the web server. |
|
||||||
| session_secret | web | very_secret | The session secret for the web frontend. |
|
| session_identifier | web | wgPortalSession | The session identifier for the web frontend. |
|
||||||
| csrf_secret | web | extremely_secret | The CSRF secret. |
|
| session_secret | web | very_secret | The session secret for the web frontend. |
|
||||||
| site_title | web | WireGuard Portal | The title that is shown in the web frontend. |
|
| csrf_secret | web | extremely_secret | The CSRF secret. |
|
||||||
| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. |
|
| site_title | web | WireGuard Portal | The title that is shown in the web frontend. |
|
||||||
| cert_file | web | | (Optional) Path to the TLS certificate file |
|
| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. |
|
||||||
| key_file | web | | (Optional) Path to the TLS certificate key file |
|
| cert_file | web | | (Optional) Path to the TLS certificate file |
|
||||||
|
| key_file | web | | (Optional) Path to the TLS certificate key file |
|
||||||
|
|
||||||
## Upgrading from V1
|
## Upgrading from V1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
apiBasePath := filepath.Join(wd, "/internal/app/api")
|
apiBasePath := filepath.Join(wd, "/internal/app/api")
|
||||||
apis := []string{"v0"}
|
apis := []string{"v0", "v1"}
|
||||||
|
|
||||||
hasError := false
|
hasError := false
|
||||||
for _, apiVersion := range apis {
|
for _, apiVersion := range apis {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import (
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/app/api/core"
|
"github.com/h44z/wg-portal/internal/app/api/core"
|
||||||
handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers"
|
handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers"
|
||||||
|
backendV1 "github.com/h44z/wg-portal/internal/app/api/v1/backend"
|
||||||
|
handlersV1 "github.com/h44z/wg-portal/internal/app/api/v1/handlers"
|
||||||
"github.com/h44z/wg-portal/internal/app/audit"
|
"github.com/h44z/wg-portal/internal/app/audit"
|
||||||
"github.com/h44z/wg-portal/internal/app/auth"
|
"github.com/h44z/wg-portal/internal/app/auth"
|
||||||
"github.com/h44z/wg-portal/internal/app/configfile"
|
"github.com/h44z/wg-portal/internal/app/configfile"
|
||||||
|
|
@ -103,7 +105,23 @@ func main() {
|
||||||
|
|
||||||
apiFrontend := handlersV0.NewRestApi(cfg, backend)
|
apiFrontend := handlersV0.NewRestApi(cfg, backend)
|
||||||
|
|
||||||
webSrv, err := core.NewServer(cfg, apiFrontend)
|
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
|
||||||
|
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
|
||||||
|
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
|
||||||
|
apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager)
|
||||||
|
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)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
go metricsServer.Run(ctx)
|
go metricsServer.Run(ctx)
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ const currentYear = ref(new Date().getFullYear())
|
||||||
href="#" role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
href="#" role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
||||||
|
<RouterLink :to="{ name: 'settings' }" class="dropdown-item" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</RouterLink>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="auth.Logout"><i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}</a>
|
<a class="dropdown-item" href="#" @click.prevent="auth.Logout"><i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,10 @@ function close() {
|
||||||
<td>{{ $t('modals.user-view.department') }}:</td>
|
<td>{{ $t('modals.user-view.department') }}:</td>
|
||||||
<td>{{selectedUser.Department}}</td>
|
<td>{{selectedUser.Department}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ $t('modals.user-view.api-enabled') }}:</td>
|
||||||
|
<td>{{selectedUser.ApiEnabled}}</td>
|
||||||
|
</tr>
|
||||||
<tr v-if="selectedUser.Disabled">
|
<tr v-if="selectedUser.Disabled">
|
||||||
<td>{{ $t('modals.user-view.disabled') }}:</td>
|
<td>{{ $t('modals.user-view.disabled') }}:</td>
|
||||||
<td>{{selectedUser.DisabledReason}}</td>
|
<td>{{selectedUser.DisabledReason}}</td>
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,8 @@ export function freshUser() {
|
||||||
Locked: false,
|
Locked: false,
|
||||||
LockedReason: "",
|
LockedReason: "",
|
||||||
|
|
||||||
|
ApiEnabled: false,
|
||||||
|
|
||||||
PeerCount: 0
|
PeerCount: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
"users": "Benutzer",
|
"users": "Benutzer",
|
||||||
"lang": "Sprache ändern",
|
"lang": "Sprache ändern",
|
||||||
"profile": "Mein Profil",
|
"profile": "Mein Profil",
|
||||||
|
"settings": "Einstellungen",
|
||||||
"login": "Anmelden",
|
"login": "Anmelden",
|
||||||
"logout": "Abmelden"
|
"logout": "Abmelden"
|
||||||
},
|
},
|
||||||
|
|
@ -167,6 +168,26 @@
|
||||||
"button-show-peer": "Show Peer",
|
"button-show-peer": "Show Peer",
|
||||||
"button-edit-peer": "Edit Peer"
|
"button-edit-peer": "Edit Peer"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"headline": "Einstellungen",
|
||||||
|
"abstract": "Hier finden Sie persönliche Einstellungen für WireGuard Portal.",
|
||||||
|
"api": {
|
||||||
|
"headline": "API Einstellungen",
|
||||||
|
"abstract": "Hier können Sie die RESTful API verwalten.",
|
||||||
|
"active-description": "Die API ist derzeit für Ihr Benutzerkonto aktiv. Alle API-Anfragen werden mit Basic Auth authentifiziert. Verwenden Sie zur Authentifizierung die folgenden Anmeldeinformationen.",
|
||||||
|
"inactive-description": "Die API ist derzeit inaktiv. Klicken Sie auf die Schaltfläche unten, um sie zu aktivieren.",
|
||||||
|
"user-label": "API Benutzername:",
|
||||||
|
"user-placeholder": "API Benutzer",
|
||||||
|
"token-label": "API Passwort:",
|
||||||
|
"token-placeholder": "API Token",
|
||||||
|
"token-created-label": "API-Zugriff gewährt seit: ",
|
||||||
|
"button-disable-title": "Deaktivieren Sie die API. Dadurch wird der aktuelle Token ungültig.",
|
||||||
|
"button-disable-text": "API deaktivieren",
|
||||||
|
"button-enable-title": "Aktivieren Sie die API, dadurch wird ein neuer Token generiert.",
|
||||||
|
"button-enable-text": "API aktivieren",
|
||||||
|
"api-link": "API Dokumentation"
|
||||||
|
}
|
||||||
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"user-view": {
|
"user-view": {
|
||||||
"headline": "User Account:",
|
"headline": "User Account:",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"lang": "Toggle Language",
|
"lang": "Toggle Language",
|
||||||
"profile": "My Profile",
|
"profile": "My Profile",
|
||||||
|
"settings": "Settings",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"logout": "Logout"
|
"logout": "Logout"
|
||||||
},
|
},
|
||||||
|
|
@ -167,6 +168,26 @@
|
||||||
"button-show-peer": "Show Peer",
|
"button-show-peer": "Show Peer",
|
||||||
"button-edit-peer": "Edit Peer"
|
"button-edit-peer": "Edit Peer"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"headline": "Settings",
|
||||||
|
"abstract": "Here you can change your personal settings.",
|
||||||
|
"api": {
|
||||||
|
"headline": "API Settings",
|
||||||
|
"abstract": "Here you can configure the RESTful API settings.",
|
||||||
|
"active-description": "The API is currently active for your user account. All API requests are authenticated with Basic Auth. Use the following credentials for authentication.",
|
||||||
|
"inactive-description": "The API is currently inactive. Press the button below to activate it.",
|
||||||
|
"user-label": "API Username:",
|
||||||
|
"user-placeholder": "The API user",
|
||||||
|
"token-label": "API Password:",
|
||||||
|
"token-placeholder": "The API token",
|
||||||
|
"token-created-label": "API access granted at: ",
|
||||||
|
"button-disable-title": "Disable API, this will invalidate the current token.",
|
||||||
|
"button-disable-text": "Disable API",
|
||||||
|
"button-enable-title": "Enable API, this will generate a new token.",
|
||||||
|
"button-enable-text": "Enable API",
|
||||||
|
"api-link": "API Documentation"
|
||||||
|
}
|
||||||
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"user-view": {
|
"user-view": {
|
||||||
"headline": "User Account:",
|
"headline": "User Account:",
|
||||||
|
|
@ -177,8 +198,9 @@
|
||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"firstname": "Firstname",
|
"firstname": "Firstname",
|
||||||
"lastname": "Lastname",
|
"lastname": "Lastname",
|
||||||
"phone": "Phone number",
|
"phone": "Phone Number",
|
||||||
"department": "Department",
|
"department": "Department",
|
||||||
|
"api-enabled": "API Access",
|
||||||
"disabled": "Account Disabled",
|
"disabled": "Account Disabled",
|
||||||
"locked": "Account Locked",
|
"locked": "Account Locked",
|
||||||
"no-peers": "User has no associated peers.",
|
"no-peers": "User has no associated peers.",
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,14 @@ const router = createRouter({
|
||||||
// this generates a separate chunk (About.[hash].js) for this route
|
// this generates a separate chunk (About.[hash].js) for this route
|
||||||
// which is lazy-loaded when the route is visited.
|
// which is lazy-loaded when the route is visited.
|
||||||
component: () => import('../views/ProfileView.vue')
|
component: () => import('../views/ProfileView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'settings',
|
||||||
|
// route level code-splitting
|
||||||
|
// this generates a separate chunk (About.[hash].js) for this route
|
||||||
|
// which is lazy-loaded when the route is visited.
|
||||||
|
component: () => import('../views/SettingsView.vue')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
linkActiveClass: "active",
|
linkActiveClass: "active",
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,34 @@ export const profileStore = defineStore({
|
||||||
this.stats = statsResponse.Stats
|
this.stats = statsResponse.Stats
|
||||||
this.statsEnabled = statsResponse.Enabled
|
this.statsEnabled = statsResponse.Enabled
|
||||||
},
|
},
|
||||||
|
async enableApi() {
|
||||||
|
this.fetching = true
|
||||||
|
let currentUser = authStore().user.Identifier
|
||||||
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
|
||||||
|
.then(this.setUser)
|
||||||
|
.catch(error => {
|
||||||
|
this.setPeers([])
|
||||||
|
console.log("Failed to activate API for ", currentUser, ": ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to activate API!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async disableApi() {
|
||||||
|
this.fetching = true
|
||||||
|
let currentUser = authStore().user.Identifier
|
||||||
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
|
||||||
|
.then(this.setUser)
|
||||||
|
.catch(error => {
|
||||||
|
this.setPeers([])
|
||||||
|
console.log("Failed to deactivate API for ", currentUser, ": ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to deactivate API!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
async LoadPeers() {
|
async LoadPeers() {
|
||||||
this.fetching = true
|
this.fetching = true
|
||||||
let currentUser = authStore().user.Identifier
|
let currentUser = authStore().user.Identifier
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
<script setup>
|
||||||
|
import PeerViewModal from "../components/PeerViewModal.vue";
|
||||||
|
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { profileStore } from "@/stores/profile";
|
||||||
|
import PeerEditModal from "@/components/PeerEditModal.vue";
|
||||||
|
import { settingsStore } from "@/stores/settings";
|
||||||
|
import { humanFileSize } from "@/helpers/utils";
|
||||||
|
import {RouterLink} from "vue-router";
|
||||||
|
import {authStore} from "../stores/auth";
|
||||||
|
|
||||||
|
const profile = profileStore()
|
||||||
|
const settings = settingsStore()
|
||||||
|
const auth = authStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await profile.LoadUser()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>{{ $t('settings.headline') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="lead">{{ $t('settings.abstract') }}</p>
|
||||||
|
|
||||||
|
<div v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')">
|
||||||
|
<div class="bg-light p-5" v-if="profile.user.ApiToken">
|
||||||
|
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||||
|
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
<p>{{ $t('settings.api.active-description') }}</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('settings.api.user-label') }}</label>
|
||||||
|
<input v-model="profile.user.Identifier" class="form-control" :placeholder="$t('settings.api.user-placeholder')" type="text" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('settings.api.token-label') }}</label>
|
||||||
|
<input v-model="profile.user.ApiToken" class="form-control" :placeholder="$t('settings.api.token-placeholder')" type="text" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<p class="form-label mt-4">{{ $t('settings.api.token-created-label') }} {{profile.user.ApiTokenCreated}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col-6">
|
||||||
|
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-disable-title')" @click.prevent="profile.disableApi()" :disabled="profile.isFetching">
|
||||||
|
<i class="fa-solid fa-minus-circle"></i> {{ $t('settings.api.button-disable-text') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<a href="/api/v1/doc.html" target="_blank" :alt="$t('settings.api.api-link')">{{ $t('settings.api.api-link') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-light p-5" v-else>
|
||||||
|
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||||
|
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
<p>{{ $t('settings.api.inactive-description') }}</p>
|
||||||
|
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-enable-title')" @click.prevent="profile.enableApi()" :disabled="profile.isFetching">
|
||||||
|
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.api.button-enable-text') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -698,6 +698,30 @@ func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domai
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
|
var users []domain.User
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Where("email = ?", email).Find(&users).Error
|
||||||
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) == 0 {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) > 1 {
|
||||||
|
return nil, fmt.Errorf("found multiple users with email %s: %w", email, domain.ErrNotUnique)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := users[0]
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"swagger": "2.0",
|
"swagger": "2.0",
|
||||||
"info": {
|
"info": {
|
||||||
"description": "WireGuard Portal API - a testing API endpoint",
|
"description": "WireGuard Portal API - UI Endpoints",
|
||||||
"title": "WireGuard Portal API",
|
"title": "WireGuard Portal SPA-UI API",
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "WireGuard Portal Developers",
|
"name": "WireGuard Portal Developers",
|
||||||
"url": "https://github.com/h44z/wg-portal"
|
"url": "https://github.com/h44z/wg-portal"
|
||||||
|
|
@ -175,6 +175,26 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/config/settings": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Configuration"
|
||||||
|
],
|
||||||
|
"summary": "Get the frontend settings object.",
|
||||||
|
"operationId": "config_handleSettingsGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The JavaScript contents",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/csrf": {
|
"/csrf": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
|
@ -499,6 +519,91 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/interface/{id}/apply-peer-defaults": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Interface"
|
||||||
|
],
|
||||||
|
"summary": "Apply all peer defaults to the available peers.",
|
||||||
|
"operationId": "interfaces_handleApplyPeerDefaultsPost",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The interface identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The interface data",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Interface"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if applying peer defaults was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/interface/{id}/save-config": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Interface"
|
||||||
|
],
|
||||||
|
"summary": "Save the interface configuration in wg-quick format to a file.",
|
||||||
|
"operationId": "interfaces_handleSaveConfigPost",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The interface identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if saving the configuration was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/now": {
|
"/now": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Nothing more to describe...",
|
"description": "Nothing more to describe...",
|
||||||
|
|
@ -526,9 +631,50 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/peer/config-mail": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Peer"
|
||||||
|
],
|
||||||
|
"summary": "Send peer configuration via email.",
|
||||||
|
"operationId": "peers_handleEmailPost",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "The peer mail request data",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.PeerMailRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if mail sending was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/peer/config-qr/{id}": {
|
"/peer/config-qr/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
"image/png",
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -536,11 +682,20 @@
|
||||||
],
|
],
|
||||||
"summary": "Get peer configuration as qr code.",
|
"summary": "Get peer configuration as qr code.",
|
||||||
"operationId": "peers_handleQrCodeGet",
|
"operationId": "peers_handleQrCodeGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The peer identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "file"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
|
|
@ -568,6 +723,15 @@
|
||||||
],
|
],
|
||||||
"summary": "Get peer configuration as string.",
|
"summary": "Get peer configuration as string.",
|
||||||
"operationId": "peers_handleConfigGet",
|
"operationId": "peers_handleConfigGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The peer identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
|
|
@ -634,6 +798,59 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/peer/iface/{iface}/multiplenew": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Peer"
|
||||||
|
],
|
||||||
|
"summary": "Create multiple new peers for the given interface.",
|
||||||
|
"operationId": "peers_handleCreateMultiplePost",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The interface identifier",
|
||||||
|
"name": "iface",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The peer creation request data",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.MultiPeerRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.Peer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/peer/iface/{iface}/new": {
|
"/peer/iface/{iface}/new": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
|
@ -725,6 +942,47 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/peer/iface/{iface}/stats": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Peer"
|
||||||
|
],
|
||||||
|
"summary": "Get peer stats for the given interface.",
|
||||||
|
"operationId": "peers_handleStatsGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The interface identifier",
|
||||||
|
"name": "iface",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.PeerStats"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/peer/{id}": {
|
"/peer/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
|
@ -1041,6 +1299,70 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/user/{id}/api/disable": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Disable the REST API for the given user.",
|
||||||
|
"operationId": "users_handleApiDisablePost",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/{id}/api/enable": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Enable the REST API for the given user.",
|
||||||
|
"operationId": "users_handleApiEnablePost",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/user/{id}/peers": {
|
"/user/{id}/peers": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
|
@ -1061,6 +1383,44 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/{id}/stats": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Get peer stats for the given user.",
|
||||||
|
"operationId": "users_handleStatsGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.PeerStats"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|
@ -1072,6 +1432,53 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"model.ConfigOption-array_string": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Overridable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.ConfigOption-int": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Overridable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.ConfigOption-string": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Overridable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.ConfigOption-uint32": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Overridable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"Value": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model.Error": {
|
"model.Error": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -1083,25 +1490,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"model.Int32ConfigOption": {
|
"model.ExpiryDate": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"Overridable": {
|
"time.Time": {
|
||||||
"type": "boolean"
|
"type": "string"
|
||||||
},
|
|
||||||
"Value": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model.IntConfigOption": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"Overridable": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"Value": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -1290,6 +1683,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"model.MultiPeerRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Identifiers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Suffix": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model.Peer": {
|
"model.Peer": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -1304,7 +1711,7 @@
|
||||||
"description": "all allowed ip subnets, comma seperated",
|
"description": "all allowed ip subnets, comma seperated",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringSliceConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-array_string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1328,7 +1735,7 @@
|
||||||
"description": "the dns server that should be set if the interface is up, comma separated",
|
"description": "the dns server that should be set if the interface is up, comma separated",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringSliceConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-array_string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1336,7 +1743,7 @@
|
||||||
"description": "the dns search option string that should be set if the interface is up, will be appended to DnsStr",
|
"description": "the dns search option string that should be set if the interface is up, will be appended to DnsStr",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringSliceConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-array_string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1344,7 +1751,7 @@
|
||||||
"description": "the endpoint address",
|
"description": "the endpoint address",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1352,13 +1759,17 @@
|
||||||
"description": "the endpoint public key",
|
"description": "the endpoint public key",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ExpiresAt": {
|
"ExpiresAt": {
|
||||||
"description": "expiry dates for peers",
|
"description": "expiry dates for peers",
|
||||||
"type": "string"
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/model.ExpiryDate"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"ExtraAllowedIPs": {
|
"ExtraAllowedIPs": {
|
||||||
"description": "all allowed ip subnets on the server side, comma seperated",
|
"description": "all allowed ip subnets on the server side, comma seperated",
|
||||||
|
|
@ -1371,7 +1782,7 @@
|
||||||
"description": "a firewall mark",
|
"description": "a firewall mark",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.Int32ConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-uint32"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1392,7 +1803,7 @@
|
||||||
"description": "the device MTU",
|
"description": "the device MTU",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.IntConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-int"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1404,7 +1815,7 @@
|
||||||
"description": "the persistent keep-alive interval",
|
"description": "the persistent keep-alive interval",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.IntConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-int"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1412,7 +1823,7 @@
|
||||||
"description": "action that is executed after the device is down",
|
"description": "action that is executed after the device is down",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1420,7 +1831,7 @@
|
||||||
"description": "action that is executed after the device is up",
|
"description": "action that is executed after the device is up",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1428,7 +1839,7 @@
|
||||||
"description": "action that is executed before the device is down",
|
"description": "action that is executed before the device is down",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1436,7 +1847,7 @@
|
||||||
"description": "action that is executed before the device is up",
|
"description": "action that is executed before the device is up",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1458,7 +1869,7 @@
|
||||||
"description": "the routing table",
|
"description": "the routing table",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/model.StringConfigOption"
|
"$ref": "#/definitions/model.ConfigOption-string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1468,6 +1879,66 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"model.PeerMailRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Identifiers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LinkOnly": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.PeerStatData": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"BytesReceived": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"BytesTransmitted": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"EndpointAddress": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"IsConnected": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"IsPingable": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"LastHandshake": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"LastPing": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"LastSessionStart": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.PeerStats": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Enabled": {
|
||||||
|
"description": "peer stats tracking enabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"example": true
|
||||||
|
},
|
||||||
|
"Stats": {
|
||||||
|
"description": "stats, map key = Peer identifier",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/definitions/model.PeerStatData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model.SessionInfo": {
|
"model.SessionInfo": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -1491,34 +1962,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"model.StringConfigOption": {
|
"model.Settings": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"Overridable": {
|
"ApiAdminOnly": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"Value": {
|
"MailLinkOnly": {
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model.StringSliceConfigOption": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"Overridable": {
|
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"Value": {
|
"PersistentConfigSupported": {
|
||||||
"type": "array",
|
"type": "boolean"
|
||||||
"items": {
|
},
|
||||||
"type": "string"
|
"SelfProvisioning": {
|
||||||
}
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"model.User": {
|
"model.User": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"ApiEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"ApiToken": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ApiTokenCreated": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"Department": {
|
"Department": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -1545,6 +2017,14 @@
|
||||||
"Lastname": {
|
"Lastname": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"Locked": {
|
||||||
|
"description": "if this field is set, the user is locked",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"LockedReason": {
|
||||||
|
"description": "the reason why the user has been locked",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"Notes": {
|
"Notes": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,35 @@
|
||||||
basePath: /api/v0
|
basePath: /api/v0
|
||||||
definitions:
|
definitions:
|
||||||
|
model.ConfigOption-array_string:
|
||||||
|
properties:
|
||||||
|
Overridable:
|
||||||
|
type: boolean
|
||||||
|
Value:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
model.ConfigOption-int:
|
||||||
|
properties:
|
||||||
|
Overridable:
|
||||||
|
type: boolean
|
||||||
|
Value:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
model.ConfigOption-string:
|
||||||
|
properties:
|
||||||
|
Overridable:
|
||||||
|
type: boolean
|
||||||
|
Value:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
model.ConfigOption-uint32:
|
||||||
|
properties:
|
||||||
|
Overridable:
|
||||||
|
type: boolean
|
||||||
|
Value:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
model.Error:
|
model.Error:
|
||||||
properties:
|
properties:
|
||||||
Code:
|
Code:
|
||||||
|
|
@ -7,19 +37,10 @@ definitions:
|
||||||
Message:
|
Message:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
model.Int32ConfigOption:
|
model.ExpiryDate:
|
||||||
properties:
|
properties:
|
||||||
Overridable:
|
time.Time:
|
||||||
type: boolean
|
type: string
|
||||||
Value:
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
model.IntConfigOption:
|
|
||||||
properties:
|
|
||||||
Overridable:
|
|
||||||
type: boolean
|
|
||||||
Value:
|
|
||||||
type: integer
|
|
||||||
type: object
|
type: object
|
||||||
model.Interface:
|
model.Interface:
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -160,6 +181,15 @@ definitions:
|
||||||
example: /auth/google/login
|
example: /auth/google/login
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
model.MultiPeerRequest:
|
||||||
|
properties:
|
||||||
|
Identifiers:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
Suffix:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
model.Peer:
|
model.Peer:
|
||||||
properties:
|
properties:
|
||||||
Addresses:
|
Addresses:
|
||||||
|
|
@ -169,7 +199,7 @@ definitions:
|
||||||
type: array
|
type: array
|
||||||
AllowedIPs:
|
AllowedIPs:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringSliceConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-array_string'
|
||||||
description: all allowed ip subnets, comma seperated
|
description: all allowed ip subnets, comma seperated
|
||||||
CheckAliveAddress:
|
CheckAliveAddress:
|
||||||
description: optional ip address or DNS name that is used for ping checks
|
description: optional ip address or DNS name that is used for ping checks
|
||||||
|
|
@ -185,25 +215,26 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
Dns:
|
Dns:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringSliceConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-array_string'
|
||||||
description: the dns server that should be set if the interface is up, comma
|
description: the dns server that should be set if the interface is up, comma
|
||||||
separated
|
separated
|
||||||
DnsSearch:
|
DnsSearch:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringSliceConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-array_string'
|
||||||
description: the dns search option string that should be set if the interface
|
description: the dns search option string that should be set if the interface
|
||||||
is up, will be appended to DnsStr
|
is up, will be appended to DnsStr
|
||||||
Endpoint:
|
Endpoint:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: the endpoint address
|
description: the endpoint address
|
||||||
EndpointPublicKey:
|
EndpointPublicKey:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: the endpoint public key
|
description: the endpoint public key
|
||||||
ExpiresAt:
|
ExpiresAt:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/model.ExpiryDate'
|
||||||
description: expiry dates for peers
|
description: expiry dates for peers
|
||||||
type: string
|
|
||||||
ExtraAllowedIPs:
|
ExtraAllowedIPs:
|
||||||
description: all allowed ip subnets on the server side, comma seperated
|
description: all allowed ip subnets on the server side, comma seperated
|
||||||
items:
|
items:
|
||||||
|
|
@ -211,7 +242,7 @@ definitions:
|
||||||
type: array
|
type: array
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.Int32ConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-uint32'
|
||||||
description: a firewall mark
|
description: a firewall mark
|
||||||
Identifier:
|
Identifier:
|
||||||
description: peer unique identifier
|
description: peer unique identifier
|
||||||
|
|
@ -225,30 +256,30 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
Mtu:
|
Mtu:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.IntConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-int'
|
||||||
description: the device MTU
|
description: the device MTU
|
||||||
Notes:
|
Notes:
|
||||||
description: a note field for peers
|
description: a note field for peers
|
||||||
type: string
|
type: string
|
||||||
PersistentKeepalive:
|
PersistentKeepalive:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.IntConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-int'
|
||||||
description: the persistent keep-alive interval
|
description: the persistent keep-alive interval
|
||||||
PostDown:
|
PostDown:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: action that is executed after the device is down
|
description: action that is executed after the device is down
|
||||||
PostUp:
|
PostUp:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: action that is executed after the device is up
|
description: action that is executed after the device is up
|
||||||
PreDown:
|
PreDown:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: action that is executed before the device is down
|
description: action that is executed before the device is down
|
||||||
PreUp:
|
PreUp:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: action that is executed before the device is up
|
description: action that is executed before the device is up
|
||||||
PresharedKey:
|
PresharedKey:
|
||||||
description: the pre-shared Key of the peer
|
description: the pre-shared Key of the peer
|
||||||
|
|
@ -263,12 +294,52 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
RoutingTable:
|
RoutingTable:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.StringConfigOption'
|
- $ref: '#/definitions/model.ConfigOption-string'
|
||||||
description: the routing table
|
description: the routing table
|
||||||
UserIdentifier:
|
UserIdentifier:
|
||||||
description: the owner
|
description: the owner
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
model.PeerMailRequest:
|
||||||
|
properties:
|
||||||
|
Identifiers:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
LinkOnly:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
model.PeerStatData:
|
||||||
|
properties:
|
||||||
|
BytesReceived:
|
||||||
|
type: integer
|
||||||
|
BytesTransmitted:
|
||||||
|
type: integer
|
||||||
|
EndpointAddress:
|
||||||
|
type: string
|
||||||
|
IsConnected:
|
||||||
|
type: boolean
|
||||||
|
IsPingable:
|
||||||
|
type: boolean
|
||||||
|
LastHandshake:
|
||||||
|
type: string
|
||||||
|
LastPing:
|
||||||
|
type: string
|
||||||
|
LastSessionStart:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
model.PeerStats:
|
||||||
|
properties:
|
||||||
|
Enabled:
|
||||||
|
description: peer stats tracking enabled
|
||||||
|
example: true
|
||||||
|
type: boolean
|
||||||
|
Stats:
|
||||||
|
additionalProperties:
|
||||||
|
$ref: '#/definitions/model.PeerStatData'
|
||||||
|
description: stats, map key = Peer identifier
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
model.SessionInfo:
|
model.SessionInfo:
|
||||||
properties:
|
properties:
|
||||||
IsAdmin:
|
IsAdmin:
|
||||||
|
|
@ -284,24 +355,25 @@ definitions:
|
||||||
UserLastname:
|
UserLastname:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
model.StringConfigOption:
|
model.Settings:
|
||||||
properties:
|
properties:
|
||||||
Overridable:
|
ApiAdminOnly:
|
||||||
type: boolean
|
type: boolean
|
||||||
Value:
|
MailLinkOnly:
|
||||||
type: string
|
type: boolean
|
||||||
type: object
|
PersistentConfigSupported:
|
||||||
model.StringSliceConfigOption:
|
type: boolean
|
||||||
properties:
|
SelfProvisioning:
|
||||||
Overridable:
|
|
||||||
type: boolean
|
type: boolean
|
||||||
Value:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
type: object
|
type: object
|
||||||
model.User:
|
model.User:
|
||||||
properties:
|
properties:
|
||||||
|
ApiEnabled:
|
||||||
|
type: boolean
|
||||||
|
ApiToken:
|
||||||
|
type: string
|
||||||
|
ApiTokenCreated:
|
||||||
|
type: string
|
||||||
Department:
|
Department:
|
||||||
type: string
|
type: string
|
||||||
Disabled:
|
Disabled:
|
||||||
|
|
@ -320,6 +392,12 @@ definitions:
|
||||||
type: boolean
|
type: boolean
|
||||||
Lastname:
|
Lastname:
|
||||||
type: string
|
type: string
|
||||||
|
Locked:
|
||||||
|
description: if this field is set, the user is locked
|
||||||
|
type: boolean
|
||||||
|
LockedReason:
|
||||||
|
description: the reason why the user has been locked
|
||||||
|
type: string
|
||||||
Notes:
|
Notes:
|
||||||
type: string
|
type: string
|
||||||
Password:
|
Password:
|
||||||
|
|
@ -337,8 +415,8 @@ info:
|
||||||
contact:
|
contact:
|
||||||
name: WireGuard Portal Developers
|
name: WireGuard Portal Developers
|
||||||
url: https://github.com/h44z/wg-portal
|
url: https://github.com/h44z/wg-portal
|
||||||
description: WireGuard Portal API - a testing API endpoint
|
description: WireGuard Portal API - UI Endpoints
|
||||||
title: WireGuard Portal API
|
title: WireGuard Portal SPA-UI API
|
||||||
version: "0.0"
|
version: "0.0"
|
||||||
paths:
|
paths:
|
||||||
/auth/{provider}/callback:
|
/auth/{provider}/callback:
|
||||||
|
|
@ -448,6 +526,19 @@ paths:
|
||||||
summary: Get the dynamic frontend configuration javascript.
|
summary: Get the dynamic frontend configuration javascript.
|
||||||
tags:
|
tags:
|
||||||
- Configuration
|
- Configuration
|
||||||
|
/config/settings:
|
||||||
|
get:
|
||||||
|
operationId: config_handleSettingsGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The JavaScript contents
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Get the frontend settings object.
|
||||||
|
tags:
|
||||||
|
- Configuration
|
||||||
/csrf:
|
/csrf:
|
||||||
get:
|
get:
|
||||||
operationId: base_handleCsrfGet
|
operationId: base_handleCsrfGet
|
||||||
|
|
@ -536,6 +627,62 @@ paths:
|
||||||
summary: Update the interface record.
|
summary: Update the interface record.
|
||||||
tags:
|
tags:
|
||||||
- Interface
|
- Interface
|
||||||
|
/interface/{id}/apply-peer-defaults:
|
||||||
|
post:
|
||||||
|
operationId: interfaces_handleApplyPeerDefaultsPost
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: The interface data
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Interface'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if applying peer defaults was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Apply all peer defaults to the available peers.
|
||||||
|
tags:
|
||||||
|
- Interface
|
||||||
|
/interface/{id}/save-config:
|
||||||
|
post:
|
||||||
|
operationId: interfaces_handleSaveConfigPost
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if saving the configuration was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Save the interface configuration in wg-quick format to a file.
|
||||||
|
tags:
|
||||||
|
- Interface
|
||||||
/interface/all:
|
/interface/all:
|
||||||
get:
|
get:
|
||||||
operationId: interfaces_handleAllGet
|
operationId: interfaces_handleAllGet
|
||||||
|
|
@ -762,16 +909,49 @@ paths:
|
||||||
summary: Update the given peer record.
|
summary: Update the given peer record.
|
||||||
tags:
|
tags:
|
||||||
- Peer
|
- Peer
|
||||||
|
/peer/config-mail:
|
||||||
|
post:
|
||||||
|
operationId: peers_handleEmailPost
|
||||||
|
parameters:
|
||||||
|
- description: The peer mail request data
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.PeerMailRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if mail sending was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Send peer configuration via email.
|
||||||
|
tags:
|
||||||
|
- Peer
|
||||||
/peer/config-qr/{id}:
|
/peer/config-qr/{id}:
|
||||||
get:
|
get:
|
||||||
operationId: peers_handleQrCodeGet
|
operationId: peers_handleQrCodeGet
|
||||||
|
parameters:
|
||||||
|
- description: The peer identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
|
- image/png
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: file
|
||||||
"400":
|
"400":
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
|
|
@ -786,6 +966,12 @@ paths:
|
||||||
/peer/config/{id}:
|
/peer/config/{id}:
|
||||||
get:
|
get:
|
||||||
operationId: peers_handleConfigGet
|
operationId: peers_handleConfigGet
|
||||||
|
parameters:
|
||||||
|
- description: The peer identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
|
@ -833,6 +1019,41 @@ paths:
|
||||||
summary: Get peers for the given interface.
|
summary: Get peers for the given interface.
|
||||||
tags:
|
tags:
|
||||||
- Peer
|
- Peer
|
||||||
|
/peer/iface/{iface}/multiplenew:
|
||||||
|
post:
|
||||||
|
operationId: peers_handleCreateMultiplePost
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier
|
||||||
|
in: path
|
||||||
|
name: iface
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: The peer creation request data
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.MultiPeerRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.Peer'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Create multiple new peers for the given interface.
|
||||||
|
tags:
|
||||||
|
- Peer
|
||||||
/peer/iface/{iface}/new:
|
/peer/iface/{iface}/new:
|
||||||
post:
|
post:
|
||||||
operationId: peers_handleCreatePost
|
operationId: peers_handleCreatePost
|
||||||
|
|
@ -893,6 +1114,33 @@ paths:
|
||||||
summary: Prepare a new peer for the given interface.
|
summary: Prepare a new peer for the given interface.
|
||||||
tags:
|
tags:
|
||||||
- Peer
|
- Peer
|
||||||
|
/peer/iface/{iface}/stats:
|
||||||
|
get:
|
||||||
|
operationId: peers_handleStatsGet
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier
|
||||||
|
in: path
|
||||||
|
name: iface
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.PeerStats'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Get peer stats for the given interface.
|
||||||
|
tags:
|
||||||
|
- Peer
|
||||||
/user/{id}:
|
/user/{id}:
|
||||||
delete:
|
delete:
|
||||||
operationId: users_handleDelete
|
operationId: users_handleDelete
|
||||||
|
|
@ -972,6 +1220,48 @@ paths:
|
||||||
summary: Update the user record.
|
summary: Update the user record.
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
|
/user/{id}/api/disable:
|
||||||
|
post:
|
||||||
|
operationId: users_handleApiDisablePost
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.User'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Disable the REST API for the given user.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
|
/user/{id}/api/enable:
|
||||||
|
post:
|
||||||
|
operationId: users_handleApiEnablePost
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.User'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Enable the REST API for the given user.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
/user/{id}/peers:
|
/user/{id}/peers:
|
||||||
get:
|
get:
|
||||||
operationId: users_handlePeersGet
|
operationId: users_handlePeersGet
|
||||||
|
|
@ -984,6 +1274,10 @@ paths:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/model.Peer'
|
$ref: '#/definitions/model.Peer'
|
||||||
type: array
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
|
|
@ -991,6 +1285,27 @@ paths:
|
||||||
summary: Get peers for the given user.
|
summary: Get peers for the given user.
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
|
/user/{id}/stats:
|
||||||
|
get:
|
||||||
|
operationId: users_handleStatsGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.PeerStats'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Get peer stats for the given user.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
/user/all:
|
/user/all:
|
||||||
get:
|
get:
|
||||||
operationId: users_handleAllGet
|
operationId: users_handleAllGet
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -8,10 +8,16 @@
|
||||||
<rapi-doc
|
<rapi-doc
|
||||||
spec-url="{{ $.ApiSpecUrl }}"
|
spec-url="{{ $.ApiSpecUrl }}"
|
||||||
theme="dark"
|
theme="dark"
|
||||||
|
render-style="focused"
|
||||||
allow-server-selection="false"
|
allow-server-selection="false"
|
||||||
allow-authentication="false"
|
allow-authentication="true"
|
||||||
load-fonts="false"
|
load-fonts="false"
|
||||||
|
schema-style="table"
|
||||||
schema-expand-level="1"
|
schema-expand-level="1"
|
||||||
|
default-schema-tab="model"
|
||||||
|
fill-request-fields-with-example="true"
|
||||||
|
show-method-in-nav-bar="as-colored-block"
|
||||||
|
show-components="true"
|
||||||
allow-spec-url-load="false"
|
allow-spec-url-load="false"
|
||||||
allow-spec-file-load="false"
|
allow-spec-file-load="false"
|
||||||
allow-spec-file-download="true"
|
allow-spec-file-download="true"
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,8 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server,
|
||||||
s.server.StaticFS("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
s.server.StaticFS("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes
|
||||||
|
s.server.UseRawPath = true
|
||||||
|
s.server.UnescapePathValues = true
|
||||||
s.setupRoutes(endpoints...)
|
s.setupRoutes(endpoints...)
|
||||||
s.setupFrontendRoutes()
|
s.setupFrontendRoutes()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-contrib/sessions/memstore"
|
"github.com/gin-contrib/sessions/memstore"
|
||||||
|
|
@ -10,8 +13,6 @@ import (
|
||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
csrf "github.com/utrack/gin-csrf"
|
csrf "github.com/utrack/gin-csrf"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type handler interface {
|
type handler interface {
|
||||||
|
|
@ -20,12 +21,12 @@ type handler interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// To compile the API documentation use the
|
// To compile the API documentation use the
|
||||||
// build_tool
|
// api_build_tool
|
||||||
// command that can be found in the $PROJECT_ROOT/internal/ports/api/build_tool directory.
|
// command that can be found in the $PROJECT_ROOT/cmd/api_build_tool directory.
|
||||||
|
|
||||||
// @title WireGuard Portal API
|
// @title WireGuard Portal SPA-UI API
|
||||||
// @version 0.0
|
// @version 0.0
|
||||||
// @description WireGuard Portal API - a testing API endpoint
|
// @description WireGuard Portal API - UI Endpoints
|
||||||
|
|
||||||
// @contact.name WireGuard Portal Developers
|
// @contact.name WireGuard Portal Developers
|
||||||
// @contact.url https://github.com/h44z/wg-portal
|
// @contact.url https://github.com/h44z/wg-portal
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
|
||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed frontend_config.js.gotpl
|
//go:embed frontend_config.js.gotpl
|
||||||
|
|
@ -63,7 +64,8 @@ func (e configEndpoint) handleConfigJsGet() gin.HandlerFunc {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
||||||
}
|
}
|
||||||
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host, port) // override if request comes from frontend started with npm run dev
|
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host,
|
||||||
|
port) // override if request comes from frontend started with npm run dev
|
||||||
}
|
}
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
err := e.tpl.ExecuteTemplate(buf, "frontend_config.js.gotpl", gin.H{
|
err := e.tpl.ExecuteTemplate(buf, "frontend_config.js.gotpl", gin.H{
|
||||||
|
|
@ -96,6 +98,7 @@ func (e configEndpoint) handleSettingsGet() gin.HandlerFunc {
|
||||||
MailLinkOnly: e.app.Config.Mail.LinkOnly,
|
MailLinkOnly: e.app.Config.Mail.LinkOnly,
|
||||||
PersistentConfigSupported: e.app.Config.Advanced.ConfigStoragePath != "",
|
PersistentConfigSupported: e.app.Config.Advanced.ConfigStoragePath != "",
|
||||||
SelfProvisioning: e.app.Config.Core.SelfProvisioningAllowed,
|
SelfProvisioning: e.app.Config.Core.SelfProvisioningAllowed,
|
||||||
|
ApiAdminOnly: e.app.Config.Advanced.ApiAdminOnly,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type peerEndpoint struct {
|
type peerEndpoint struct {
|
||||||
|
|
@ -57,7 +58,8 @@ func (e peerEndpoint) handleAllGet() gin.HandlerFunc {
|
||||||
|
|
||||||
_, peers, err := e.app.GetInterfaceAndPeers(ctx, domain.InterfaceIdentifier(interfaceId))
|
_, peers, err := e.app.GetInterfaceAndPeers(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,7 +90,8 @@ func (e peerEndpoint) handleSingleGet() gin.HandlerFunc {
|
||||||
|
|
||||||
peer, err := e.app.GetPeer(ctx, domain.PeerIdentifier(peerId))
|
peer, err := e.app.GetPeer(ctx, domain.PeerIdentifier(peerId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,7 +122,8 @@ func (e peerEndpoint) handlePrepareGet() gin.HandlerFunc {
|
||||||
|
|
||||||
peer, err := e.app.PreparePeer(ctx, domain.InterfaceIdentifier(interfaceId))
|
peer, err := e.app.PreparePeer(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +167,8 @@ func (e peerEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||||
|
|
||||||
newPeer, err := e.app.CreatePeer(ctx, model.NewDomainPeer(&p))
|
newPeer, err := e.app.CreatePeer(ctx, model.NewDomainPeer(&p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,9 +205,11 @@ func (e peerEndpoint) handleCreateMultiplePost() gin.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newPeers, err := e.app.CreateMultiplePeers(ctx, domain.InterfaceIdentifier(interfaceId), model.NewDomainPeerCreationRequest(&req))
|
newPeers, err := e.app.CreateMultiplePeers(ctx, domain.InterfaceIdentifier(interfaceId),
|
||||||
|
model.NewDomainPeerCreationRequest(&req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,7 +253,8 @@ func (e peerEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||||
|
|
||||||
updatedPeer, err := e.app.UpdatePeer(ctx, model.NewDomainPeer(&p))
|
updatedPeer, err := e.app.UpdatePeer(ctx, model.NewDomainPeer(&p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,7 +285,8 @@ func (e peerEndpoint) handleDelete() gin.HandlerFunc {
|
||||||
|
|
||||||
err := e.app.DeletePeer(ctx, domain.PeerIdentifier(id))
|
err := e.app.DeletePeer(ctx, domain.PeerIdentifier(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,9 +342,10 @@ func (e peerEndpoint) handleConfigGet() gin.HandlerFunc {
|
||||||
// @ID peers_handleQrCodeGet
|
// @ID peers_handleQrCodeGet
|
||||||
// @Tags Peer
|
// @Tags Peer
|
||||||
// @Summary Get peer configuration as qr code.
|
// @Summary Get peer configuration as qr code.
|
||||||
|
// @Produce png
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "The peer identifier"
|
// @Param id path string true "The peer identifier"
|
||||||
// @Success 200 {object} string
|
// @Success 200 {file} binary
|
||||||
// @Failure 400 {object} model.Error
|
// @Failure 400 {object} model.Error
|
||||||
// @Failure 500 {object} model.Error
|
// @Failure 500 {object} model.Error
|
||||||
// @Router /peer/config-qr/{id} [get]
|
// @Router /peer/config-qr/{id} [get]
|
||||||
|
|
@ -403,7 +413,8 @@ func (e peerEndpoint) handleEmailPost() gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
err = e.app.SendPeerEmail(ctx, req.LinkOnly, peerIds...)
|
err = e.app.SendPeerEmail(ctx, req.LinkOnly, peerIds...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -434,7 +445,8 @@ func (e peerEndpoint) handleStatsGet() gin.HandlerFunc {
|
||||||
|
|
||||||
stats, err := e.app.GetPeerStats(ctx, domain.InterfaceIdentifier(interfaceId))
|
stats, err := e.app.GetPeerStats(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type userEndpoint struct {
|
type userEndpoint struct {
|
||||||
|
|
@ -27,6 +28,8 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
|
||||||
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
|
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
|
||||||
apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
|
apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
|
||||||
|
apiGroup.POST("/:id/api/enable", e.authenticator.UserIdMatch("id"), e.handleApiEnablePost())
|
||||||
|
apiGroup.POST("/:id/api/disable", e.authenticator.UserIdMatch("id"), e.handleApiDisablePost())
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAllGet returns a gorm handler function.
|
// handleAllGet returns a gorm handler function.
|
||||||
|
|
@ -44,7 +47,8 @@ func (e userEndpoint) handleAllGet() gin.HandlerFunc {
|
||||||
|
|
||||||
users, err := e.app.GetAllUsers(ctx)
|
users, err := e.app.GetAllUsers(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,11 +78,12 @@ func (e userEndpoint) handleSingleGet() gin.HandlerFunc {
|
||||||
|
|
||||||
user, err := e.app.GetUser(ctx, domain.UserIdentifier(id))
|
user, err := e.app.GetUser(ctx, domain.UserIdentifier(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewUser(user))
|
c.JSON(http.StatusOK, model.NewUser(user, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,11 +123,12 @@ func (e userEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||||
|
|
||||||
updateUser, err := e.app.UpdateUser(ctx, model.NewDomainUser(&user))
|
updateUser, err := e.app.UpdateUser(ctx, model.NewDomainUser(&user))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewUser(updateUser))
|
c.JSON(http.StatusOK, model.NewUser(updateUser, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -150,11 +156,12 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||||
|
|
||||||
newUser, err := e.app.CreateUser(ctx, model.NewDomainUser(&user))
|
newUser, err := e.app.CreateUser(ctx, model.NewDomainUser(&user))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewUser(newUser))
|
c.JSON(http.StatusOK, model.NewUser(newUser, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,13 +181,15 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
|
||||||
|
|
||||||
interfaceId := Base64UrlDecode(c.Param("id"))
|
interfaceId := Base64UrlDecode(c.Param("id"))
|
||||||
if interfaceId == "" {
|
if interfaceId == "" {
|
||||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(interfaceId))
|
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,13 +213,15 @@ func (e userEndpoint) handleStatsGet() gin.HandlerFunc {
|
||||||
|
|
||||||
userId := Base64UrlDecode(c.Param("id"))
|
userId := Base64UrlDecode(c.Param("id"))
|
||||||
if userId == "" {
|
if userId == "" {
|
||||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stats, err := e.app.GetUserPeerStats(ctx, domain.UserIdentifier(userId))
|
stats, err := e.app.GetUserPeerStats(ctx, domain.UserIdentifier(userId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,10 +252,75 @@ func (e userEndpoint) handleDelete() gin.HandlerFunc {
|
||||||
|
|
||||||
err := e.app.DeleteUser(ctx, domain.UserIdentifier(id))
|
err := e.app.DeleteUser(ctx, domain.UserIdentifier(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleApiEnablePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleApiEnablePost
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Enable the REST API for the given user.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} model.User
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/{id}/api/enable [post]
|
||||||
|
func (e userEndpoint) handleApiEnablePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
userId := Base64UrlDecode(c.Param("id"))
|
||||||
|
if userId == "" {
|
||||||
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := e.app.ActivateApi(ctx, domain.UserIdentifier(userId))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.NewUser(user, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleApiDisablePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleApiDisablePost
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Disable the REST API for the given user.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} model.User
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/{id}/api/disable [post]
|
||||||
|
func (e userEndpoint) handleApiDisablePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
userId := Base64UrlDecode(c.Param("id"))
|
||||||
|
if userId == "" {
|
||||||
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := e.app.DeactivateApi(ctx, domain.UserIdentifier(userId))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.NewUser(user, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,5 @@ type Settings struct {
|
||||||
MailLinkOnly bool `json:"MailLinkOnly"`
|
MailLinkOnly bool `json:"MailLinkOnly"`
|
||||||
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
||||||
SelfProvisioning bool `json:"SelfProvisioning"`
|
SelfProvisioning bool `json:"SelfProvisioning"`
|
||||||
|
ApiAdminOnly bool `json:"ApiAdminOnly"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,37 +25,50 @@ type User struct {
|
||||||
Locked bool `json:"Locked"` // if this field is set, the user is locked
|
Locked bool `json:"Locked"` // if this field is set, the user is locked
|
||||||
LockedReason string `json:"LockedReason"` // the reason why the user has been locked
|
LockedReason string `json:"LockedReason"` // the reason why the user has been locked
|
||||||
|
|
||||||
|
ApiToken string `json:"ApiToken"`
|
||||||
|
ApiTokenCreated *time.Time `json:"ApiTokenCreated,omitempty"`
|
||||||
|
ApiEnabled bool `json:"ApiEnabled"`
|
||||||
|
|
||||||
// Calculated
|
// Calculated
|
||||||
|
|
||||||
PeerCount int `json:"PeerCount"`
|
PeerCount int `json:"PeerCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser(src *domain.User) *User {
|
func NewUser(src *domain.User, exposeCreds bool) *User {
|
||||||
return &User{
|
u := &User{
|
||||||
Identifier: string(src.Identifier),
|
Identifier: string(src.Identifier),
|
||||||
Email: src.Email,
|
Email: src.Email,
|
||||||
Source: string(src.Source),
|
Source: string(src.Source),
|
||||||
ProviderName: src.ProviderName,
|
ProviderName: src.ProviderName,
|
||||||
IsAdmin: src.IsAdmin,
|
IsAdmin: src.IsAdmin,
|
||||||
Firstname: src.Firstname,
|
Firstname: src.Firstname,
|
||||||
Lastname: src.Lastname,
|
Lastname: src.Lastname,
|
||||||
Phone: src.Phone,
|
Phone: src.Phone,
|
||||||
Department: src.Department,
|
Department: src.Department,
|
||||||
Notes: src.Notes,
|
Notes: src.Notes,
|
||||||
Password: "", // never fill password
|
Password: "", // never fill password
|
||||||
Disabled: src.IsDisabled(),
|
Disabled: src.IsDisabled(),
|
||||||
DisabledReason: src.DisabledReason,
|
DisabledReason: src.DisabledReason,
|
||||||
Locked: src.IsLocked(),
|
Locked: src.IsLocked(),
|
||||||
LockedReason: src.LockedReason,
|
LockedReason: src.LockedReason,
|
||||||
|
ApiToken: "", // by default, do not expose API token
|
||||||
|
ApiTokenCreated: src.ApiTokenCreated,
|
||||||
|
ApiEnabled: src.IsApiEnabled(),
|
||||||
|
|
||||||
PeerCount: src.LinkedPeerCount,
|
PeerCount: src.LinkedPeerCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if exposeCreds {
|
||||||
|
u.ApiToken = src.ApiToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUsers(src []domain.User) []User {
|
func NewUsers(src []domain.User) []User {
|
||||||
results := make([]User, len(src))
|
results := make([]User, len(src))
|
||||||
for i := range src {
|
for i := range src {
|
||||||
results[i] = *NewUser(&src[i])
|
results[i] = *NewUser(&src[i], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InterfaceServiceInterfaceManagerRepo interface {
|
||||||
|
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||||
|
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
|
||||||
|
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||||
|
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
interfaces InterfaceServiceInterfaceManagerRepo
|
||||||
|
users PeerServiceUserManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterfaceService(cfg *config.Config, interfaces InterfaceServiceInterfaceManagerRepo) *InterfaceService {
|
||||||
|
return &InterfaceService{
|
||||||
|
cfg: cfg,
|
||||||
|
interfaces: interfaces,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) GetAll(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces, interfacePeers, err := s.interfaces.GetAllInterfacesAndPeers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces, interfacePeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) GetById(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
|
*domain.Interface,
|
||||||
|
[]domain.Peer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceData, interfacePeers, err := s.interfaces.GetInterfaceAndPeers(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaceData, interfacePeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) Create(ctx context.Context, iface *domain.Interface) (*domain.Interface, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createdInterface, err := s.interfaces.CreateInterface(ctx, iface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdInterface, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) Update(ctx context.Context, id domain.InterfaceIdentifier, iface *domain.Interface) (
|
||||||
|
*domain.Interface,
|
||||||
|
[]domain.Peer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if iface.Identifier != id {
|
||||||
|
return nil, nil, fmt.Errorf("interface id mismatch: %s != %s: %w",
|
||||||
|
iface.Identifier, id, domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedInterface, updatedPeers, err := s.interfaces.UpdateInterface(ctx, iface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedInterface, updatedPeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) Delete(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.interfaces.DeleteInterface(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PeerServicePeerManagerRepo interface {
|
||||||
|
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
|
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
|
||||||
|
UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
|
||||||
|
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerServiceUserManagerRepo interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
peers PeerServicePeerManagerRepo
|
||||||
|
users PeerServiceUserManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeerService(
|
||||||
|
cfg *config.Config,
|
||||||
|
peers PeerServicePeerManagerRepo,
|
||||||
|
users PeerServiceUserManagerRepo,
|
||||||
|
) *PeerService {
|
||||||
|
return &PeerService{
|
||||||
|
cfg: cfg,
|
||||||
|
peers: peers,
|
||||||
|
users: users,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) GetForInterface(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, interfacePeers, err := s.peers.GetInterfaceAndPeers(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfacePeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) GetForUser(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||||
|
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.users.GetUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userPeers, err := s.peers.GetUserPeers(ctx, user.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userPeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) GetById(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
|
||||||
|
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||||
|
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := s.peers.GetPeer(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has access rights to the requested peer.
|
||||||
|
// If the peer is not linked to any user, access is granted only for admins.
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return peer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) Create(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
|
||||||
|
return nil, fmt.Errorf("peer id mismatch: %s != %s: %w",
|
||||||
|
peer.Identifier, peer.Interface.PublicKey, domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdPeer, err := s.peers.CreatePeer(ctx, peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdPeer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) Update(ctx context.Context, _ domain.PeerIdentifier, peer *domain.Peer) (
|
||||||
|
*domain.Peer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedPeer, err := s.peers.UpdatePeer(ctx, peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedPeer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) Delete(ctx context.Context, id domain.PeerIdentifier) error {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.peers.DeletePeer(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProvisioningServiceUserManagerRepo interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningServicePeerManagerRepo interface {
|
||||||
|
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
GetUserPeers(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
|
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
|
||||||
|
CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningServiceConfigFileManagerRepo interface {
|
||||||
|
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
||||||
|
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
users ProvisioningServiceUserManagerRepo
|
||||||
|
peers ProvisioningServicePeerManagerRepo
|
||||||
|
configFiles ProvisioningServiceConfigFileManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvisioningService(
|
||||||
|
cfg *config.Config,
|
||||||
|
users ProvisioningServiceUserManagerRepo,
|
||||||
|
peers ProvisioningServicePeerManagerRepo,
|
||||||
|
configFiles ProvisioningServiceConfigFileManagerRepo,
|
||||||
|
) *ProvisioningService {
|
||||||
|
return &ProvisioningService{
|
||||||
|
cfg: cfg,
|
||||||
|
|
||||||
|
users: users,
|
||||||
|
peers: peers,
|
||||||
|
configFiles: configFiles,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) GetUserAndPeers(
|
||||||
|
ctx context.Context,
|
||||||
|
userId domain.UserIdentifier,
|
||||||
|
email string,
|
||||||
|
) (*domain.User, []domain.Peer, error) {
|
||||||
|
// first fetch user
|
||||||
|
var user *domain.User
|
||||||
|
switch {
|
||||||
|
case userId != "":
|
||||||
|
u, err := p.users.GetUser(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
user = u
|
||||||
|
case email != "":
|
||||||
|
u, err := p.users.GetUserByEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
user = u
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("either UserId or Email must be set: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, user.Identifier); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, err := p.peers.GetUserPeers(ctx, user.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, peers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) GetPeerConfig(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error) {
|
||||||
|
peer, err := p.peers.GetPeer(ctx, peerId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgData, err := io.ReadAll(peerCfgReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerCfgData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) GetPeerQrPng(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error) {
|
||||||
|
peer, err := p.peers.GetPeer(ctx, peerId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgQrData, err := io.ReadAll(peerCfgQrReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerCfgQrData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) NewPeer(ctx context.Context, req models.ProvisioningRequest) (*domain.Peer, error) {
|
||||||
|
if req.UserIdentifier == "" {
|
||||||
|
req.UserIdentifier = string(domain.GetUserInfo(ctx).Id) // use authenticated user id if not set
|
||||||
|
}
|
||||||
|
|
||||||
|
// check permissions
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, domain.UserIdentifier(req.UserIdentifier)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !p.cfg.Core.SelfProvisioningAllowed {
|
||||||
|
// only admins can create new peers if self-provisioning is disabled
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare new peer
|
||||||
|
peer, err := p.peers.PreparePeer(ctx, domain.InterfaceIdentifier(req.InterfaceIdentifier))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to prepare new peer: %w", err)
|
||||||
|
}
|
||||||
|
peer.UserIdentifier = domain.UserIdentifier(req.UserIdentifier) // overwrite context user id with the one from the request
|
||||||
|
if req.PublicKey != "" {
|
||||||
|
peer.Identifier = domain.PeerIdentifier(req.PublicKey)
|
||||||
|
peer.Interface.PublicKey = req.PublicKey
|
||||||
|
peer.Interface.PrivateKey = "" // clear private key if public key is set, WireGuard Portal does not know the private key in that case
|
||||||
|
}
|
||||||
|
if req.PresharedKey != "" {
|
||||||
|
peer.PresharedKey = domain.PreSharedKey(req.PresharedKey)
|
||||||
|
}
|
||||||
|
peer.GenerateDisplayName("API")
|
||||||
|
|
||||||
|
// save new peer
|
||||||
|
peer, err = p.peers.CreatePeer(ctx, peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new peer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return peer, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserManagerRepo interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||||
|
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
|
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
|
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
users UserManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserService(cfg *config.Config, users UserManagerRepo) *UserService {
|
||||||
|
return &UserService{
|
||||||
|
cfg: cfg,
|
||||||
|
users: users,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) GetAll(ctx context.Context) ([]domain.User, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allUsers, err := s.users.GetAllUsers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return allUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) GetById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||||
|
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.users.GetUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) Create(ctx context.Context, user *domain.User) (*domain.User, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createdUser, err := s.users.CreateUser(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) Update(ctx context.Context, id domain.UserIdentifier, user *domain.User) (
|
||||||
|
*domain.User,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != user.Identifier {
|
||||||
|
return nil, fmt.Errorf("user id mismatch: %s != %s: %w", id, user.Identifier, domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedUser, err := s.users.UpdateUser(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) Delete(ctx context.Context, id domain.UserIdentifier) error {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.users.DeleteUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/core"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
GetName() string
|
||||||
|
RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To compile the API documentation use the
|
||||||
|
// api_build_tool
|
||||||
|
// command that can be found in the $PROJECT_ROOT/cmd/api_build_tool directory.
|
||||||
|
|
||||||
|
// @title WireGuard Portal Public API
|
||||||
|
// @version 1.0
|
||||||
|
// @description The WireGuard Portal REST API enables efficient management of WireGuard VPN configurations through a set of JSON-based endpoints.
|
||||||
|
// @description It supports creating and editing peers, interfaces, and user profiles, while also providing role-based access control and auditing.
|
||||||
|
// @description This API allows seamless integration with external tools or scripts for automated network configuration and administration.
|
||||||
|
|
||||||
|
// @license.name MIT
|
||||||
|
// @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
// @contact.name WireGuard Portal Project
|
||||||
|
// @contact.url https://github.com/h44z/wg-portal
|
||||||
|
|
||||||
|
// @securityDefinitions.basic BasicAuth
|
||||||
|
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @query.collection.format multi
|
||||||
|
|
||||||
|
func NewRestApi(userSource UserSource, handlers ...Handler) core.ApiEndpointSetupFunc {
|
||||||
|
authenticator := &authenticationHandler{
|
||||||
|
userSource: userSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() (core.ApiVersion, core.GroupSetupFn) {
|
||||||
|
return "v1", func(group *gin.RouterGroup) {
|
||||||
|
group.Use(cors.Default())
|
||||||
|
|
||||||
|
// Handler functions
|
||||||
|
for _, h := range handlers {
|
||||||
|
h.RegisterRoutes(group, authenticator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseServiceError(err error) (int, models.Error) {
|
||||||
|
if err == nil {
|
||||||
|
return 500, models.Error{
|
||||||
|
Code: 500,
|
||||||
|
Message: "unknown server error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code := http.StatusInternalServerError
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, domain.ErrNotFound):
|
||||||
|
code = http.StatusNotFound
|
||||||
|
case errors.Is(err, domain.ErrNoPermission):
|
||||||
|
code = http.StatusForbidden
|
||||||
|
case errors.Is(err, domain.ErrDuplicateEntry):
|
||||||
|
code = http.StatusConflict
|
||||||
|
case errors.Is(err, domain.ErrInvalidData):
|
||||||
|
code = http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, models.Error{
|
||||||
|
Code: code,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InterfaceEndpointInterfaceService interface {
|
||||||
|
GetAll(context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||||
|
GetById(context.Context, domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
Create(context.Context, *domain.Interface) (*domain.Interface, error)
|
||||||
|
Update(context.Context, domain.InterfaceIdentifier, *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||||
|
Delete(context.Context, domain.InterfaceIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceEndpoint struct {
|
||||||
|
interfaces InterfaceEndpointInterfaceService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterfaceEndpoint(interfaceService InterfaceEndpointInterfaceService) *InterfaceEndpoint {
|
||||||
|
return &InterfaceEndpoint{
|
||||||
|
interfaces: interfaceService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e InterfaceEndpoint) GetName() string {
|
||||||
|
return "InterfaceEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e InterfaceEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/interface", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
||||||
|
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleByIdGet())
|
||||||
|
|
||||||
|
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
|
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||||
|
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID interface_handleAllGet
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Get all interface records.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.Interface
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/all [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleAllGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
allInterfaces, allPeersPerInterface, err := e.interfaces.GetAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterfaces(allInterfaces, allPeersPerInterface))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleByIdGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleByIdGet
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Get a specific interface record by its identifier.
|
||||||
|
// @Param id path string true "The interface identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Interface
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/by-id/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iface, interfacePeers, err := e.interfaces.GetById(ctx, domain.InterfaceIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterface(iface, interfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreatePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleCreatePost
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Create a new interface record.
|
||||||
|
// @Param request body models.Interface true "The interface data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Interface
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 409 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/new [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var iface models.Interface
|
||||||
|
err := c.BindJSON(&iface)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newInterface, err := e.interfaces.Create(ctx, models.NewDomainInterface(&iface))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterface(newInterface, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdatePut returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleUpdatePut
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Update an interface record.
|
||||||
|
// @Param id path string true "The interface identifier."
|
||||||
|
// @Param request body models.Interface true "The interface data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Interface
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/by-id/{id} [put]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var iface models.Interface
|
||||||
|
err := c.BindJSON(&iface)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedInterface, updatedInterfacePeers, err := e.interfaces.Update(
|
||||||
|
ctx,
|
||||||
|
domain.InterfaceIdentifier(id),
|
||||||
|
models.NewDomainInterface(&iface),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterface(updatedInterface, updatedInterfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelete returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleDelete
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Delete the interface record.
|
||||||
|
// @Param id path string true "The interface identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204 "No content if deletion was successful."
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/by-id/{id} [delete]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleDelete() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.interfaces.Delete(ctx, domain.InterfaceIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PeerService interface {
|
||||||
|
GetForInterface(context.Context, domain.InterfaceIdentifier) ([]domain.Peer, error)
|
||||||
|
GetForUser(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
|
GetById(context.Context, domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
Create(context.Context, *domain.Peer) (*domain.Peer, error)
|
||||||
|
Update(context.Context, domain.PeerIdentifier, *domain.Peer) (*domain.Peer, error)
|
||||||
|
Delete(context.Context, domain.PeerIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerEndpoint struct {
|
||||||
|
peers PeerService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeerEndpoint(peerService PeerService) *PeerEndpoint {
|
||||||
|
return &PeerEndpoint{
|
||||||
|
peers: peerService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PeerEndpoint) GetName() string {
|
||||||
|
return "PeerEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PeerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/peer", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/by-interface/:id", authenticator.LoggedIn(ScopeAdmin), e.handleAllForInterfaceGet())
|
||||||
|
apiGroup.GET("/by-user/:id", authenticator.LoggedIn(), e.handleAllForUserGet())
|
||||||
|
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
|
||||||
|
|
||||||
|
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
|
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||||
|
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllForInterfaceGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleAllForInterfaceGet
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Get all peer records for a given WireGuard interface.
|
||||||
|
// @Param id path string true "The WireGuard interface identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.Peer
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-interface/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleAllForInterfaceGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interfacePeers, err := e.peers.GetForInterface(ctx, domain.InterfaceIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllForUserGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleAllForUserGet
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Get all peer records for a given user.
|
||||||
|
// @Description Normal users can only access their own records. Admins can access all records.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.Peer
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-user/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleAllForUserGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interfacePeers, err := e.peers.GetForUser(ctx, domain.UserIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleByIdGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleByIdGet
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Get a specific peer record by its identifier (public key).
|
||||||
|
// @Description Normal users can only access their own records. Admins can access all records.
|
||||||
|
// @Param id path string true "The peer identifier (public key)."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-id/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := e.peers.GetById(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreatePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleCreatePost
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Create a new peer record.
|
||||||
|
// @Description Only admins can create new records.
|
||||||
|
// @Param request body models.Peer true "The peer data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 409 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/new [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var peer models.Peer
|
||||||
|
err := c.BindJSON(&peer)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPeer, err := e.peers.Create(ctx, models.NewDomainPeer(&peer))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(newPeer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdatePut returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleUpdatePut
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Update a peer record.
|
||||||
|
// @Description Only admins can update existing records.
|
||||||
|
// @Param id path string true "The peer identifier."
|
||||||
|
// @Param request body models.Peer true "The peer data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-id/{id} [put]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var peer models.Peer
|
||||||
|
err := c.BindJSON(&peer)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedPeer, err := e.peers.Update(ctx, domain.PeerIdentifier(id), models.NewDomainPeer(&peer))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(updatedPeer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelete returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleDelete
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Delete the peer record.
|
||||||
|
// @Param id path string true "The peer identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204 "No content if deletion was successful."
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-id/{id} [delete]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleDelete() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.peers.Delete(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProvisioningEndpointProvisioningService interface {
|
||||||
|
GetUserAndPeers(ctx context.Context, userId domain.UserIdentifier, email string) (
|
||||||
|
*domain.User,
|
||||||
|
[]domain.Peer,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
GetPeerConfig(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
|
||||||
|
GetPeerQrPng(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
|
||||||
|
NewPeer(ctx context.Context, req models.ProvisioningRequest) (*domain.Peer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningEndpoint struct {
|
||||||
|
provisioning ProvisioningEndpointProvisioningService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvisioningEndpoint(provisioning ProvisioningEndpointProvisioningService) *ProvisioningEndpoint {
|
||||||
|
return &ProvisioningEndpoint{
|
||||||
|
provisioning: provisioning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ProvisioningEndpoint) GetName() string {
|
||||||
|
return "ProvisioningEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ProvisioningEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/provisioning", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/data/user-info", authenticator.LoggedIn(), e.handleUserInfoGet())
|
||||||
|
apiGroup.GET("/data/peer-config", authenticator.LoggedIn(), e.handlePeerConfigGet())
|
||||||
|
apiGroup.GET("/data/peer-qr", authenticator.LoggedIn(), e.handlePeerQrGet())
|
||||||
|
|
||||||
|
apiGroup.POST("/new-peer", authenticator.LoggedIn(), e.handleNewPeerPost())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUserInfoGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handleUserInfoGet
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Get information about all peer records for a given user.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param UserId query string false "The user identifier that should be queried. If not set, the authenticated user is used."
|
||||||
|
// @Param Email query string false "The email address that should be queried. If UserId is set, this is ignored."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.UserInformation
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/data/user-info [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handleUserInfoGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := strings.TrimSpace(c.Query("UserId"))
|
||||||
|
email := strings.TrimSpace(c.Query("Email"))
|
||||||
|
|
||||||
|
if id == "" && email == "" {
|
||||||
|
id = string(domain.GetUserInfo(ctx).Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, peers, err := e.provisioning.GetUserAndPeers(ctx, domain.UserIdentifier(id), email)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUserInformation(user, peers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePeerConfigGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handlePeerConfigGet
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Get the peer configuration in wg-quick format.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
|
||||||
|
// @Produce plain
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {string} string "The WireGuard configuration file"
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/data/peer-config [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handlePeerConfigGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := strings.TrimSpace(c.Query("PeerId"))
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConfig, err := e.provisioning.GetPeerConfig(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, "text/plain", peerConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePeerQrGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handlePeerQrGet
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Get the peer configuration as QR code.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
|
||||||
|
// @Produce png
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {file} binary "The WireGuard configuration QR code"
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/data/peer-qr [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handlePeerQrGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := strings.TrimSpace(c.Query("PeerId"))
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConfigQrCode, err := e.provisioning.GetPeerQrPng(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, "image/png", peerConfigQrCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNewPeerPost returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handleNewPeerPost
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Create a new peer for the given interface and user.
|
||||||
|
// @Description Normal users can only create new peers if self provisioning is allowed. Admins can always add new peers.
|
||||||
|
// @Param request body models.ProvisioningRequest true "Provisioning request model."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/new-peer [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handleNewPeerPost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var req models.ProvisioningRequest
|
||||||
|
err := c.BindJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := e.provisioning.NewPeer(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserService interface {
|
||||||
|
GetAll(ctx context.Context) ([]domain.User, error)
|
||||||
|
GetById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
Create(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
|
Update(ctx context.Context, id domain.UserIdentifier, user *domain.User) (*domain.User, error)
|
||||||
|
Delete(ctx context.Context, id domain.UserIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserEndpoint struct {
|
||||||
|
users UserService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserEndpoint(userService UserService) *UserEndpoint {
|
||||||
|
return &UserEndpoint{
|
||||||
|
users: userService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UserEndpoint) GetName() string {
|
||||||
|
return "UserEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UserEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/user", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
||||||
|
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
|
||||||
|
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
|
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||||
|
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleAllGet
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Get all user records.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.User
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/all [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleAllGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
users, err := e.users.GetAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUsers(users))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleByIdGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleByIdGet
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Get a specific user record by its internal identifier.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/by-id/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := e.users.GetById(ctx, domain.UserIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUser(user, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreatePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleCreatePost
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Create a new user record.
|
||||||
|
// @Description Only admins can create new records.
|
||||||
|
// @Param request body models.User true "The user data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 409 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/new [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err := c.BindJSON(&user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newUser, err := e.users.Create(ctx, models.NewDomainUser(&user))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUser(newUser, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdatePut returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleUpdatePut
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Update a user record.
|
||||||
|
// @Description Only admins can update existing records.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Param request body models.User true "The user data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/by-id/{id} [put]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err := c.BindJSON(&user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser, err := e.users.Update(ctx, domain.UserIdentifier(id), models.NewDomainUser(&user))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUser(updateUser, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelete returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleDelete
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Delete the user record.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204 "No content if deletion was successful."
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/by-id/{id} [delete]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleDelete() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.users.Delete(ctx, domain.UserIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scope string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScopeAdmin Scope = "ADMIN" // Admin scope contains all other scopes
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserSource interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type authenticationHandler struct {
|
||||||
|
userSource UserSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggedIn checks if a user is logged in. If scopes are given, they are validated as well.
|
||||||
|
func (h authenticationHandler) LoggedIn(scopes ...Scope) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
username, password, ok := c.Request.BasicAuth()
|
||||||
|
if !ok || username == "" || password == "" {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "missing credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user exists in DB
|
||||||
|
|
||||||
|
ctx := domain.SetUserInfo(c.Request.Context(), domain.SystemAdminContextUserInfo())
|
||||||
|
user, err := h.userSource.GetUser(ctx, domain.UserIdentifier(username))
|
||||||
|
if err != nil {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate API token
|
||||||
|
if err := user.CheckApiToken(password); err != nil {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !UserHasScopes(user, scopes...) {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusForbidden, model.Error{Code: http.StatusForbidden, Message: "not enough permissions"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set(domain.CtxUserInfo, &domain.ContextUserInfo{
|
||||||
|
Id: user.Identifier,
|
||||||
|
IsAdmin: user.IsAdmin,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Continue down the chain to Handler etc
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserHasScopes(user *domain.User, scopes ...Scope) bool {
|
||||||
|
// No scopes give, so the check should succeed
|
||||||
|
if len(scopes) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user has admin scope
|
||||||
|
if user.IsAdmin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if admin scope is required
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if scope == ScopeAdmin {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigOption[T any] struct {
|
||||||
|
Value T `json:"Value"`
|
||||||
|
Overridable bool `json:"Overridable,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigOption[T any](value T, overridable bool) ConfigOption[T] {
|
||||||
|
return ConfigOption[T]{
|
||||||
|
Value: value,
|
||||||
|
Overridable: overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigOptionFromDomain[T any](opt domain.ConfigOption[T]) ConfigOption[T] {
|
||||||
|
return ConfigOption[T]{
|
||||||
|
Value: opt.Value,
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigOptionToDomain[T any](opt ConfigOption[T]) domain.ConfigOption[T] {
|
||||||
|
return domain.ConfigOption[T]{
|
||||||
|
Value: opt.Value,
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSliceConfigOptionFromDomain(opt domain.ConfigOption[string]) ConfigOption[[]string] {
|
||||||
|
return ConfigOption[[]string]{
|
||||||
|
Value: internal.SliceString(opt.Value),
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSliceConfigOptionToDomain(opt ConfigOption[[]string]) domain.ConfigOption[string] {
|
||||||
|
return domain.ConfigOption[string]{
|
||||||
|
Value: internal.SliceToString(opt.Value),
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
// Error represents an error response.
|
||||||
|
type Error struct {
|
||||||
|
Code int `json:"Code"` // HTTP status code.
|
||||||
|
Message string `json:"Message"` // Error message.
|
||||||
|
Details string `json:"Details,omitempty"` // Additional error details.
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface represents a WireGuard interface.
|
||||||
|
type Interface struct {
|
||||||
|
// Identifier is the unique identifier of the interface. It is always equal to the device name of the interface.
|
||||||
|
Identifier string `json:"Identifier" example:"wg0" binding:"required"`
|
||||||
|
// DisplayName is a nice display name / description for the interface.
|
||||||
|
DisplayName string `json:"DisplayName" binding:"omitempty,max=64" example:"My Interface"`
|
||||||
|
// Mode is the interface type, either 'server', 'client' or 'any'. The mode specifies how WireGuard Portal handles peers for this interface.
|
||||||
|
Mode string `json:"Mode" example:"server" binding:"required,oneof=server client any"`
|
||||||
|
// PrivateKey is the private key of the interface.
|
||||||
|
PrivateKey string `json:"PrivateKey" example:"gI6EdUSYvn8ugXOt8QQD6Yc+JyiZxIhp3GInSWRfWGE=" binding:"required,len=44"`
|
||||||
|
// PublicKey is the public key of the server interface. The public key is used by peers to connect to the server.
|
||||||
|
PublicKey string `json:"PublicKey" example:"HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=" binding:"required,len=44"`
|
||||||
|
// Disabled is a flag that specifies if the interface is enabled (up) or not (down). Disabled interfaces are not able to accept connections.
|
||||||
|
Disabled bool `json:"Disabled" example:"false"`
|
||||||
|
// DisabledReason is the reason why the interface has been disabled.
|
||||||
|
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:"This is a reason why the interface has been disabled."`
|
||||||
|
// SaveConfig is a flag that specifies if the configuration should be saved to the configuration file (wgX.conf in wg-quick format).
|
||||||
|
SaveConfig bool `json:"SaveConfig" example:"false"`
|
||||||
|
|
||||||
|
// ListenPort is the listening port, for example: 51820. The listening port is only required for server interfaces.
|
||||||
|
ListenPort int `json:"ListenPort" binding:"omitempty,min=1,max=65535" example:"51820"`
|
||||||
|
// Addresses is a list of IP addresses (in CIDR format) that are assigned to the interface.
|
||||||
|
Addresses []string `json:"Addresses" binding:"omitempty,dive,cidr" example:"10.11.12.1/24"`
|
||||||
|
// Dns is a list of DNS servers that should be set if the interface is up.
|
||||||
|
Dns []string `json:"Dns" binding:"omitempty,dive,ip" example:"1.1.1.1"`
|
||||||
|
// DnsSearch is the dns search option string that should be set if the interface is up, will be appended to Dns servers.
|
||||||
|
DnsSearch []string `json:"DnsSearch" binding:"omitempty,dive,fqdn" example:"wg.local"`
|
||||||
|
// Mtu is the device MTU of the interface.
|
||||||
|
Mtu int `json:"Mtu" binding:"omitempty,min=1,max=9000" example:"1420"`
|
||||||
|
// FirewallMark is an optional firewall mark which is used to handle interface traffic.
|
||||||
|
FirewallMark uint32 `json:"FirewallMark"`
|
||||||
|
// RoutingTable is an optional routing table which is used to route interface traffic.
|
||||||
|
RoutingTable string `json:"RoutingTable"`
|
||||||
|
|
||||||
|
// PreUp is an optional action that is executed before the device is up.
|
||||||
|
PreUp string `json:"PreUp" example:"echo 'Interface is up'"`
|
||||||
|
// PostUp is an optional action that is executed after the device is up.
|
||||||
|
PostUp string `json:"PostUp" example:"iptables -A FORWARD -i %i -j ACCEPT"`
|
||||||
|
// PreDown is an optional action that is executed before the device is down.
|
||||||
|
PreDown string `json:"PreDown" example:"iptables -D FORWARD -i %i -j ACCEPT"`
|
||||||
|
// PostDown is an optional action that is executed after the device is down.
|
||||||
|
PostDown string `json:"PostDown" example:"echo 'Interface is down'"`
|
||||||
|
|
||||||
|
// PeerDefNetwork specifies the default subnets from which new peers will get their IP addresses. The subnet is specified in CIDR format.
|
||||||
|
PeerDefNetwork []string `json:"PeerDefNetwork" example:"10.11.12.0/24"`
|
||||||
|
// PeerDefDns specifies the default dns servers for a new peer.
|
||||||
|
PeerDefDns []string `json:"PeerDefDns" example:"8.8.8.8"`
|
||||||
|
// PeerDefDnsSearch specifies the default dns search options for a new peer.
|
||||||
|
PeerDefDnsSearch []string `json:"PeerDefDnsSearch" example:"wg.local"`
|
||||||
|
// PeerDefEndpoint specifies the default endpoint for a new peer.
|
||||||
|
PeerDefEndpoint string `json:"PeerDefEndpoint" example:"wg.example.com:51820"`
|
||||||
|
// PeerDefAllowedIPs specifies the default allowed IP addresses for a new peer.
|
||||||
|
PeerDefAllowedIPs []string `json:"PeerDefAllowedIPs" example:"10.11.12.0/24"`
|
||||||
|
// PeerDefMtu specifies the default device MTU for a new peer.
|
||||||
|
PeerDefMtu int `json:"PeerDefMtu" example:"1420"`
|
||||||
|
// PeerDefPersistentKeepalive specifies the default persistent keep-alive value in seconds for a new peer.
|
||||||
|
PeerDefPersistentKeepalive int `json:"PeerDefPersistentKeepalive" example:"25"`
|
||||||
|
// PeerDefFirewallMark specifies the default firewall mark for a new peer.
|
||||||
|
PeerDefFirewallMark uint32 `json:"PeerDefFirewallMark"`
|
||||||
|
// PeerDefRoutingTable specifies the default routing table for a new peer.
|
||||||
|
PeerDefRoutingTable string `json:"PeerDefRoutingTable"`
|
||||||
|
|
||||||
|
// PeerDefPreUp specifies the default action that is executed before the device is up for a new peer.
|
||||||
|
PeerDefPreUp string `json:"PeerDefPreUp"`
|
||||||
|
// PeerDefPostUp specifies the default action that is executed after the device is up for a new peer.
|
||||||
|
PeerDefPostUp string `json:"PeerDefPostUp"`
|
||||||
|
// PeerDefPreDown specifies the default action that is executed before the device is down for a new peer.
|
||||||
|
PeerDefPreDown string `json:"PeerDefPreDown"`
|
||||||
|
// PeerDefPostDown specifies the default action that is executed after the device is down for a new peer.
|
||||||
|
PeerDefPostDown string `json:"PeerDefPostDown"`
|
||||||
|
|
||||||
|
// Calculated values
|
||||||
|
|
||||||
|
// EnabledPeers is the number of enabled peers for this interface. Only enabled peers are able to connect.
|
||||||
|
EnabledPeers int `json:"EnabledPeers" readonly:"true"`
|
||||||
|
// TotalPeers is the total number of peers for this interface.
|
||||||
|
TotalPeers int `json:"TotalPeers" readonly:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
|
||||||
|
iface := &Interface{
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
Mode: string(src.Type),
|
||||||
|
PrivateKey: src.PrivateKey,
|
||||||
|
PublicKey: src.PublicKey,
|
||||||
|
Disabled: src.IsDisabled(),
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
SaveConfig: src.SaveConfig,
|
||||||
|
ListenPort: src.ListenPort,
|
||||||
|
Addresses: domain.CidrsToStringSlice(src.Addresses),
|
||||||
|
Dns: internal.SliceString(src.DnsStr),
|
||||||
|
DnsSearch: internal.SliceString(src.DnsSearchStr),
|
||||||
|
Mtu: src.Mtu,
|
||||||
|
FirewallMark: src.FirewallMark,
|
||||||
|
RoutingTable: src.RoutingTable,
|
||||||
|
PreUp: src.PreUp,
|
||||||
|
PostUp: src.PostUp,
|
||||||
|
PreDown: src.PreDown,
|
||||||
|
PostDown: src.PostDown,
|
||||||
|
PeerDefNetwork: internal.SliceString(src.PeerDefNetworkStr),
|
||||||
|
PeerDefDns: internal.SliceString(src.PeerDefDnsStr),
|
||||||
|
PeerDefDnsSearch: internal.SliceString(src.PeerDefDnsSearchStr),
|
||||||
|
PeerDefEndpoint: src.PeerDefEndpoint,
|
||||||
|
PeerDefAllowedIPs: internal.SliceString(src.PeerDefAllowedIPsStr),
|
||||||
|
PeerDefMtu: src.PeerDefMtu,
|
||||||
|
PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive,
|
||||||
|
PeerDefFirewallMark: src.PeerDefFirewallMark,
|
||||||
|
PeerDefRoutingTable: src.PeerDefRoutingTable,
|
||||||
|
PeerDefPreUp: src.PeerDefPreUp,
|
||||||
|
PeerDefPostUp: src.PeerDefPostUp,
|
||||||
|
PeerDefPreDown: src.PeerDefPreDown,
|
||||||
|
PeerDefPostDown: src.PeerDefPostDown,
|
||||||
|
|
||||||
|
EnabledPeers: 0,
|
||||||
|
TotalPeers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(peers) > 0 {
|
||||||
|
iface.TotalPeers = len(peers)
|
||||||
|
|
||||||
|
activePeers := 0
|
||||||
|
for _, peer := range peers {
|
||||||
|
if !peer.IsDisabled() {
|
||||||
|
activePeers++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iface.EnabledPeers = activePeers
|
||||||
|
}
|
||||||
|
|
||||||
|
return iface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface {
|
||||||
|
results := make([]Interface, len(src))
|
||||||
|
for i := range src {
|
||||||
|
results[i] = *NewInterface(&src[i], srcPeers[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainInterface(src *Interface) *domain.Interface {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
cidrs, _ := domain.CidrsFromArray(src.Addresses)
|
||||||
|
|
||||||
|
res := &domain.Interface{
|
||||||
|
BaseModel: domain.BaseModel{},
|
||||||
|
Identifier: domain.InterfaceIdentifier(src.Identifier),
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PrivateKey: src.PrivateKey,
|
||||||
|
PublicKey: src.PublicKey,
|
||||||
|
},
|
||||||
|
ListenPort: src.ListenPort,
|
||||||
|
Addresses: cidrs,
|
||||||
|
DnsStr: internal.SliceToString(src.Dns),
|
||||||
|
DnsSearchStr: internal.SliceToString(src.DnsSearch),
|
||||||
|
Mtu: src.Mtu,
|
||||||
|
FirewallMark: src.FirewallMark,
|
||||||
|
RoutingTable: src.RoutingTable,
|
||||||
|
PreUp: src.PreUp,
|
||||||
|
PostUp: src.PostUp,
|
||||||
|
PreDown: src.PreDown,
|
||||||
|
PostDown: src.PostDown,
|
||||||
|
SaveConfig: src.SaveConfig,
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
Type: domain.InterfaceType(src.Mode),
|
||||||
|
DriverType: "", // currently unused
|
||||||
|
Disabled: nil, // set below
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
PeerDefNetworkStr: internal.SliceToString(src.PeerDefNetwork),
|
||||||
|
PeerDefDnsStr: internal.SliceToString(src.PeerDefDns),
|
||||||
|
PeerDefDnsSearchStr: internal.SliceToString(src.PeerDefDnsSearch),
|
||||||
|
PeerDefEndpoint: src.PeerDefEndpoint,
|
||||||
|
PeerDefAllowedIPsStr: internal.SliceToString(src.PeerDefAllowedIPs),
|
||||||
|
PeerDefMtu: src.PeerDefMtu,
|
||||||
|
PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive,
|
||||||
|
PeerDefFirewallMark: src.PeerDefFirewallMark,
|
||||||
|
PeerDefRoutingTable: src.PeerDefRoutingTable,
|
||||||
|
PeerDefPreUp: src.PeerDefPreUp,
|
||||||
|
PeerDefPostUp: src.PeerDefPostUp,
|
||||||
|
PeerDefPreDown: src.PeerDefPreDown,
|
||||||
|
PeerDefPostDown: src.PeerDefPostDown,
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Disabled {
|
||||||
|
res.Disabled = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ExpiryDateTimeLayout = "\"2006-01-02\""
|
||||||
|
|
||||||
|
type ExpiryDate struct {
|
||||||
|
*time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON will unmarshal using 2006-01-02 layout
|
||||||
|
func (d *ExpiryDate) UnmarshalJSON(b []byte) error {
|
||||||
|
if len(b) == 0 || string(b) == "null" || string(b) == "\"\"" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(ExpiryDateTimeLayout, string(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parsed.IsZero() {
|
||||||
|
d.Time = &parsed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON will marshal using 2006-01-02 layout
|
||||||
|
func (d *ExpiryDate) MarshalJSON() ([]byte, error) {
|
||||||
|
if d == nil || d.Time == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s := d.Format(ExpiryDateTimeLayout)
|
||||||
|
return []byte(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peer represents a WireGuard peer entry.
|
||||||
|
type Peer struct {
|
||||||
|
// Identifier is the unique identifier of the peer. It is always equal to the public key of the peer.
|
||||||
|
Identifier string `json:"Identifier" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"required,len=44"`
|
||||||
|
// DisplayName is a nice display name / description for the peer.
|
||||||
|
DisplayName string `json:"DisplayName" example:"My Peer" binding:"omitempty,max=64"`
|
||||||
|
// UserIdentifier is the identifier of the user that owns the peer.
|
||||||
|
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||||
|
// InterfaceIdentifier is the identifier of the interface the peer is linked to.
|
||||||
|
InterfaceIdentifier string `json:"InterfaceIdentifier" binding:"required" example:"wg0"`
|
||||||
|
// Disabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
|
||||||
|
Disabled bool `json:"Disabled" example:"false"`
|
||||||
|
// DisabledReason is the reason why the peer has been disabled.
|
||||||
|
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:"This is a reason why the peer has been disabled."`
|
||||||
|
// ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. An expired peer is not able to connect.
|
||||||
|
ExpiresAt ExpiryDate `json:"ExpiresAt,omitempty" binding:"omitempty,datetime=2006-01-02"`
|
||||||
|
// Notes is a note field for peers.
|
||||||
|
Notes string `json:"Notes" example:"This is a note for the peer."`
|
||||||
|
|
||||||
|
// Endpoint is the endpoint address of the peer.
|
||||||
|
Endpoint ConfigOption[string] `json:"Endpoint"`
|
||||||
|
// EndpointPublicKey is the endpoint public key.
|
||||||
|
EndpointPublicKey ConfigOption[string] `json:"EndpointPublicKey"`
|
||||||
|
// AllowedIPs is a list of allowed IP subnets for the peer.
|
||||||
|
AllowedIPs ConfigOption[[]string] `json:"AllowedIPs"`
|
||||||
|
// ExtraAllowedIPs is a list of additional allowed IP subnets for the peer. These allowed IP subnets are added on the server side.
|
||||||
|
ExtraAllowedIPs []string `json:"ExtraAllowedIPs"`
|
||||||
|
// PresharedKey is the optional pre-shared Key of the peer.
|
||||||
|
PresharedKey string `json:"PresharedKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"omitempty,len=44"`
|
||||||
|
// PersistentKeepalive is the optional persistent keep-alive interval in seconds.
|
||||||
|
PersistentKeepalive ConfigOption[int] `json:"PersistentKeepalive" binding:"omitempty,gte=0"`
|
||||||
|
|
||||||
|
// PrivateKey is the private Key of the peer.
|
||||||
|
PrivateKey string `json:"PrivateKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"required,len=44"`
|
||||||
|
// PublicKey is the public Key of the server peer.
|
||||||
|
PublicKey string `json:"PublicKey" example:"TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=" binding:"omitempty,len=44"`
|
||||||
|
|
||||||
|
// Mode is the peer interface type (server, client, any).
|
||||||
|
Mode string `json:"Mode" example:"client" binding:"omitempty,oneof=server client any"`
|
||||||
|
|
||||||
|
// Addresses is a list of IP addresses in CIDR format (both IPv4 and IPv6) for the peer.
|
||||||
|
Addresses []string `json:"Addresses" example:"10.11.12.2/24" binding:"omitempty,dive,cidr"`
|
||||||
|
// CheckAliveAddress is an optional ip address or DNS name that is used for ping checks.
|
||||||
|
CheckAliveAddress string `json:"CheckAliveAddress" binding:"omitempty,ip|fqdn" example:"1.1.1.1"`
|
||||||
|
// Dns is a list of DNS servers that should be set if the peer interface is up.
|
||||||
|
Dns ConfigOption[[]string] `json:"Dns"`
|
||||||
|
// DnsSearch is the dns search option string that should be set if the peer interface is up, will be appended to Dns servers.
|
||||||
|
DnsSearch ConfigOption[[]string] `json:"DnsSearch"`
|
||||||
|
// Mtu is the device MTU of the peer.
|
||||||
|
Mtu ConfigOption[int] `json:"Mtu"`
|
||||||
|
// FirewallMark is an optional firewall mark which is used to handle peer traffic.
|
||||||
|
FirewallMark ConfigOption[uint32] `json:"FirewallMark"`
|
||||||
|
// RoutingTable is an optional routing table which is used to route peer traffic.
|
||||||
|
RoutingTable ConfigOption[string] `json:"RoutingTable"`
|
||||||
|
|
||||||
|
// PreUp is an optional action that is executed before the device is up.
|
||||||
|
PreUp ConfigOption[string] `json:"PreUp"`
|
||||||
|
// PostUp is an optional action that is executed after the device is up.
|
||||||
|
PostUp ConfigOption[string] `json:"PostUp"`
|
||||||
|
// PreDown is an optional action that is executed before the device is down.
|
||||||
|
PreDown ConfigOption[string] `json:"PreDown"`
|
||||||
|
// PostDown is an optional action that is executed after the device is down.
|
||||||
|
PostDown ConfigOption[string] `json:"PostDown"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeer(src *domain.Peer) *Peer {
|
||||||
|
return &Peer{
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
UserIdentifier: string(src.UserIdentifier),
|
||||||
|
InterfaceIdentifier: string(src.InterfaceIdentifier),
|
||||||
|
Disabled: src.IsDisabled(),
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
ExpiresAt: ExpiryDate{src.ExpiresAt},
|
||||||
|
Notes: src.Notes,
|
||||||
|
Endpoint: ConfigOptionFromDomain(src.Endpoint),
|
||||||
|
EndpointPublicKey: ConfigOptionFromDomain(src.EndpointPublicKey),
|
||||||
|
AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr),
|
||||||
|
ExtraAllowedIPs: internal.SliceString(src.ExtraAllowedIPsStr),
|
||||||
|
PresharedKey: string(src.PresharedKey),
|
||||||
|
PersistentKeepalive: ConfigOptionFromDomain(src.PersistentKeepalive),
|
||||||
|
PrivateKey: src.Interface.PrivateKey,
|
||||||
|
PublicKey: src.Interface.PublicKey,
|
||||||
|
Mode: string(src.Interface.Type),
|
||||||
|
Addresses: domain.CidrsToStringSlice(src.Interface.Addresses),
|
||||||
|
CheckAliveAddress: src.Interface.CheckAliveAddress,
|
||||||
|
Dns: StringSliceConfigOptionFromDomain(src.Interface.DnsStr),
|
||||||
|
DnsSearch: StringSliceConfigOptionFromDomain(src.Interface.DnsSearchStr),
|
||||||
|
Mtu: ConfigOptionFromDomain(src.Interface.Mtu),
|
||||||
|
FirewallMark: ConfigOptionFromDomain(src.Interface.FirewallMark),
|
||||||
|
RoutingTable: ConfigOptionFromDomain(src.Interface.RoutingTable),
|
||||||
|
PreUp: ConfigOptionFromDomain(src.Interface.PreUp),
|
||||||
|
PostUp: ConfigOptionFromDomain(src.Interface.PostUp),
|
||||||
|
PreDown: ConfigOptionFromDomain(src.Interface.PreDown),
|
||||||
|
PostDown: ConfigOptionFromDomain(src.Interface.PostDown),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeers(src []domain.Peer) []Peer {
|
||||||
|
results := make([]Peer, len(src))
|
||||||
|
for i := range src {
|
||||||
|
results[i] = *NewPeer(&src[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainPeer(src *Peer) *domain.Peer {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
cidrs, _ := domain.CidrsFromArray(src.Addresses)
|
||||||
|
|
||||||
|
res := &domain.Peer{
|
||||||
|
BaseModel: domain.BaseModel{},
|
||||||
|
Endpoint: ConfigOptionToDomain(src.Endpoint),
|
||||||
|
EndpointPublicKey: ConfigOptionToDomain(src.EndpointPublicKey),
|
||||||
|
AllowedIPsStr: StringSliceConfigOptionToDomain(src.AllowedIPs),
|
||||||
|
ExtraAllowedIPsStr: internal.SliceToString(src.ExtraAllowedIPs),
|
||||||
|
PresharedKey: domain.PreSharedKey(src.PresharedKey),
|
||||||
|
PersistentKeepalive: ConfigOptionToDomain(src.PersistentKeepalive),
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
Identifier: domain.PeerIdentifier(src.Identifier),
|
||||||
|
UserIdentifier: domain.UserIdentifier(src.UserIdentifier),
|
||||||
|
InterfaceIdentifier: domain.InterfaceIdentifier(src.InterfaceIdentifier),
|
||||||
|
Disabled: nil, // set below
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
ExpiresAt: src.ExpiresAt.Time,
|
||||||
|
Notes: src.Notes,
|
||||||
|
Interface: domain.PeerInterfaceConfig{
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PrivateKey: src.PrivateKey,
|
||||||
|
PublicKey: src.PublicKey,
|
||||||
|
},
|
||||||
|
Type: domain.InterfaceType(src.Mode),
|
||||||
|
Addresses: cidrs,
|
||||||
|
CheckAliveAddress: src.CheckAliveAddress,
|
||||||
|
DnsStr: StringSliceConfigOptionToDomain(src.Dns),
|
||||||
|
DnsSearchStr: StringSliceConfigOptionToDomain(src.DnsSearch),
|
||||||
|
Mtu: ConfigOptionToDomain(src.Mtu),
|
||||||
|
FirewallMark: ConfigOptionToDomain(src.FirewallMark),
|
||||||
|
RoutingTable: ConfigOptionToDomain(src.RoutingTable),
|
||||||
|
PreUp: ConfigOptionToDomain(src.PreUp),
|
||||||
|
PostUp: ConfigOptionToDomain(src.PostUp),
|
||||||
|
PreDown: ConfigOptionToDomain(src.PreDown),
|
||||||
|
PostDown: ConfigOptionToDomain(src.PostDown),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Disabled {
|
||||||
|
res.Disabled = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "github.com/h44z/wg-portal/internal/domain"
|
||||||
|
|
||||||
|
// UserInformation represents the information about a user and its linked peers.
|
||||||
|
type UserInformation struct {
|
||||||
|
// UserIdentifier is the unique identifier of the user.
|
||||||
|
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||||
|
// PeerCount is the number of peers linked to the user.
|
||||||
|
PeerCount int `json:"PeerCount" example:"2"`
|
||||||
|
// Peers is a list of peers linked to the user.
|
||||||
|
Peers []UserInformationPeer `json:"Peers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInformationPeer represents the information about a peer.
|
||||||
|
type UserInformationPeer struct {
|
||||||
|
// Identifier is the unique identifier of the peer. It equals the public key of the peer.
|
||||||
|
Identifier string `json:"Identifier" example:"peer-1234567"`
|
||||||
|
// DisplayName is a user-defined description of the peer.
|
||||||
|
DisplayName string `json:"DisplayName" example:"My iPhone"`
|
||||||
|
// IPAddresses is a list of IP addresses in CIDR format assigned to the peer.
|
||||||
|
IpAddresses []string `json:"IpAddresses" example:"10.11.12.2/24"`
|
||||||
|
// IsDisabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
|
||||||
|
IsDisabled bool `json:"IsDisabled,omitempty" example:"true"`
|
||||||
|
|
||||||
|
// InterfaceIdentifier is the unique identifier of the WireGuard Portal device the peer is connected to.
|
||||||
|
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserInformation(user *domain.User, peers []domain.Peer) *UserInformation {
|
||||||
|
if user == nil {
|
||||||
|
return &UserInformation{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui := &UserInformation{
|
||||||
|
UserIdentifier: string(user.Identifier),
|
||||||
|
PeerCount: len(peers),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range peers {
|
||||||
|
ui.Peers = append(ui.Peers, NewUserInformationPeer(peer))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ui.Peers) == 0 {
|
||||||
|
ui.Peers = []UserInformationPeer{} // Ensure that the JSON output is an empty array instead of null.
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserInformationPeer(peer domain.Peer) UserInformationPeer {
|
||||||
|
up := UserInformationPeer{
|
||||||
|
Identifier: string(peer.Identifier),
|
||||||
|
DisplayName: peer.DisplayName,
|
||||||
|
IpAddresses: domain.CidrsToStringSlice(peer.Interface.Addresses),
|
||||||
|
IsDisabled: peer.IsDisabled(),
|
||||||
|
InterfaceIdentifier: string(peer.InterfaceIdentifier),
|
||||||
|
}
|
||||||
|
|
||||||
|
return up
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvisioningRequest represents a request to provision a new peer.
|
||||||
|
type ProvisioningRequest struct {
|
||||||
|
// InterfaceIdentifier is the identifier of the WireGuard interface the peer should be linked to.
|
||||||
|
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0" binding:"required"`
|
||||||
|
// UserIdentifier is the identifier of the user the peer should be linked to.
|
||||||
|
// If no user identifier is set, the authenticated user is used.
|
||||||
|
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||||
|
|
||||||
|
// PublicKey is the optional public key of the peer. If no public key is set, a new key pair is generated.
|
||||||
|
PublicKey string `json:"PublicKey" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"omitempty,len=44"`
|
||||||
|
// PresharedKey is the optional pre-shared key of the peer. If no pre-shared key is set, a new key is generated.
|
||||||
|
PresharedKey string `json:"PresharedKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"omitempty,len=44"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User represents a user in the system.
|
||||||
|
type User struct {
|
||||||
|
// The unique identifier of the user.
|
||||||
|
Identifier string `json:"Identifier" binding:"required,max=64" example:"uid-1234567"`
|
||||||
|
// The email address of the user. This field is optional.
|
||||||
|
Email string `json:"Email" binding:"omitempty,email" example:"test@test.com"`
|
||||||
|
// The source of the user. This field is optional.
|
||||||
|
Source string `json:"Source" binding:"oneof=db" example:"db"`
|
||||||
|
// The name of the authentication provider. This field is read-only.
|
||||||
|
ProviderName string `json:"ProviderName,omitempty" readonly:"true" example:""`
|
||||||
|
// If this field is set, the user is an admin.
|
||||||
|
IsAdmin bool `json:"IsAdmin" binding:"required" example:"false"`
|
||||||
|
|
||||||
|
// The first name of the user. This field is optional.
|
||||||
|
Firstname string `json:"Firstname" example:"Max"`
|
||||||
|
// The last name of the user. This field is optional.
|
||||||
|
Lastname string `json:"Lastname" example:"Muster"`
|
||||||
|
// The phone number of the user. This field is optional.
|
||||||
|
Phone string `json:"Phone" example:"+1234546789"`
|
||||||
|
// The department of the user. This field is optional.
|
||||||
|
Department string `json:"Department" example:"Software Development"`
|
||||||
|
// Additional notes about the user. This field is optional.
|
||||||
|
Notes string `json:"Notes" example:"some sample notes"`
|
||||||
|
|
||||||
|
// The password of the user. This field is never populated on read operations.
|
||||||
|
Password string `json:"Password,omitempty" binding:"omitempty,min=16,max=64" example:""`
|
||||||
|
// If this field is set, the user is disabled.
|
||||||
|
Disabled bool `json:"Disabled" example:"false"`
|
||||||
|
// The reason why the user has been disabled.
|
||||||
|
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:""`
|
||||||
|
// If this field is set, the user is locked and thus unable to log in to WireGuard Portal.
|
||||||
|
Locked bool `json:"Locked" example:"false"`
|
||||||
|
// The reason why the user has been locked.
|
||||||
|
LockedReason string `json:"LockedReason" binding:"required_if=Locked true" example:""`
|
||||||
|
|
||||||
|
// The API token of the user. This field is never populated on bulk read operations.
|
||||||
|
ApiToken string `json:"ApiToken,omitempty" binding:"omitempty,min=32,max=64" example:""`
|
||||||
|
// If this field is set, the user is allowed to use the RESTful API. This field is read-only.
|
||||||
|
ApiEnabled bool `json:"ApiEnabled" readonly:"true" example:"false"`
|
||||||
|
|
||||||
|
// The number of peers linked to the user. This field is read-only.
|
||||||
|
PeerCount int `json:"PeerCount" readonly:"true" example:"2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUser(src *domain.User, exposeCredentials bool) *User {
|
||||||
|
u := &User{
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
Email: src.Email,
|
||||||
|
Source: string(src.Source),
|
||||||
|
ProviderName: src.ProviderName,
|
||||||
|
IsAdmin: src.IsAdmin,
|
||||||
|
Firstname: src.Firstname,
|
||||||
|
Lastname: src.Lastname,
|
||||||
|
Phone: src.Phone,
|
||||||
|
Department: src.Department,
|
||||||
|
Notes: src.Notes,
|
||||||
|
Password: "", // never fill password
|
||||||
|
Disabled: src.IsDisabled(),
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
Locked: src.IsLocked(),
|
||||||
|
LockedReason: src.LockedReason,
|
||||||
|
ApiToken: "", // by default, do not expose API token
|
||||||
|
ApiEnabled: src.IsApiEnabled(),
|
||||||
|
PeerCount: src.LinkedPeerCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
if exposeCredentials {
|
||||||
|
u.ApiToken = src.ApiToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUsers(src []domain.User) []User {
|
||||||
|
results := make([]User, len(src))
|
||||||
|
for i := range src {
|
||||||
|
results[i] = *NewUser(&src[i], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainUser(src *User) *domain.User {
|
||||||
|
now := time.Now()
|
||||||
|
res := &domain.User{
|
||||||
|
Identifier: domain.UserIdentifier(src.Identifier),
|
||||||
|
Email: src.Email,
|
||||||
|
Source: domain.UserSource(src.Source),
|
||||||
|
ProviderName: src.ProviderName,
|
||||||
|
IsAdmin: src.IsAdmin,
|
||||||
|
Firstname: src.Firstname,
|
||||||
|
Lastname: src.Lastname,
|
||||||
|
Phone: src.Phone,
|
||||||
|
Department: src.Department,
|
||||||
|
Notes: src.Notes,
|
||||||
|
Password: domain.PrivateString(src.Password),
|
||||||
|
Disabled: nil, // set below
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
Locked: nil, // set below
|
||||||
|
LockedReason: src.LockedReason,
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.ApiToken != "" {
|
||||||
|
res.ApiToken = src.ApiToken
|
||||||
|
res.ApiTokenCreated = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Disabled {
|
||||||
|
res.Disabled = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Locked {
|
||||||
|
res.Locked = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
@ -22,9 +22,19 @@ type App struct {
|
||||||
StatisticsCollector
|
StatisticsCollector
|
||||||
ConfigFileManager
|
ConfigFileManager
|
||||||
MailManager
|
MailManager
|
||||||
|
ApiV1Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config, bus evbus.MessageBus, authenticator Authenticator, users UserManager, wireGuard WireGuardManager, stats StatisticsCollector, cfgFiles ConfigFileManager, mailer MailManager) (*App, error) {
|
func New(
|
||||||
|
cfg *config.Config,
|
||||||
|
bus evbus.MessageBus,
|
||||||
|
authenticator Authenticator,
|
||||||
|
users UserManager,
|
||||||
|
wireGuard WireGuardManager,
|
||||||
|
stats StatisticsCollector,
|
||||||
|
cfgFiles ConfigFileManager,
|
||||||
|
mailer MailManager,
|
||||||
|
) (*App, error) {
|
||||||
|
|
||||||
a := &App{
|
a := &App{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
|
|
@ -60,7 +70,7 @@ func New(cfg *config.Config, bus evbus.MessageBus, authenticator Authenticator,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Startup(ctx context.Context) error {
|
func (a *App) Startup(ctx context.Context) error {
|
||||||
|
|
||||||
a.UserManager.StartBackgroundJobs(ctx)
|
a.UserManager.StartBackgroundJobs(ctx)
|
||||||
a.StatisticsCollector.StartBackgroundJobs(ctx)
|
a.StatisticsCollector.StartBackgroundJobs(ctx)
|
||||||
a.WireGuardManager.StartBackgroundJobs(ctx)
|
a.WireGuardManager.StartBackgroundJobs(ctx)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package app
|
package app
|
||||||
|
|
||||||
const TopicUserCreated = "user:created"
|
const TopicUserCreated = "user:created"
|
||||||
|
const TopicUserApiEnabled = "user:api:enabled"
|
||||||
|
const TopicUserApiDisabled = "user:api:disabled"
|
||||||
const TopicUserRegistered = "user:registered"
|
const TopicUserRegistered = "user:registered"
|
||||||
const TopicUserDisabled = "user:disabled"
|
const TopicUserDisabled = "user:disabled"
|
||||||
const TopicUserEnabled = "user:enabled"
|
const TopicUserEnabled = "user:enabled"
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Authenticator interface {
|
type Authenticator interface {
|
||||||
|
|
@ -23,6 +24,8 @@ type UserManager interface {
|
||||||
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||||
|
ActivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WireGuardManager interface {
|
type WireGuardManager interface {
|
||||||
|
|
@ -43,7 +46,11 @@ type WireGuardManager interface {
|
||||||
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
|
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
|
||||||
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||||
CreateMultiplePeers(ctx context.Context, id domain.InterfaceIdentifier, r *domain.PeerCreationRequest) ([]domain.Peer, error)
|
CreateMultiplePeers(
|
||||||
|
ctx context.Context,
|
||||||
|
id domain.InterfaceIdentifier,
|
||||||
|
r *domain.PeerCreationRequest,
|
||||||
|
) ([]domain.Peer, error)
|
||||||
UpdatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
UpdatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||||
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||||
ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error
|
ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error
|
||||||
|
|
@ -63,3 +70,7 @@ type ConfigFileManager interface {
|
||||||
type MailManager interface {
|
type MailManager interface {
|
||||||
SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error
|
SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ApiV1Manager interface {
|
||||||
|
ApiV1GetUsers(ctx context.Context) ([]domain.User, error)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@ package users
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserDatabaseRepo interface {
|
type UserDatabaseRepo interface {
|
||||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||||
FindUsers(ctx context.Context, search string) ([]domain.User, error)
|
FindUsers(ctx context.Context, search string) ([]domain.User, error)
|
||||||
SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error
|
SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
|
@ -101,7 +102,7 @@ func (m Manager) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain
|
||||||
|
|
||||||
user, err := m.users.GetUser(ctx, id)
|
user, err := m.users.GetUser(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to load peer %s: %w", id, err)
|
return nil, fmt.Errorf("unable to load user %s: %w", id, err)
|
||||||
}
|
}
|
||||||
peers, _ := m.peers.GetUserPeers(ctx, id) // ignore error, list will be empty in error case
|
peers, _ := m.peers.GetUserPeers(ctx, id) // ignore error, list will be empty in error case
|
||||||
|
|
||||||
|
|
@ -110,6 +111,24 @@ func (m Manager) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Manager) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
|
|
||||||
|
user, err := m.users.GetUserByEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to load user for email %s: %w", email, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, user.Identifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, _ := m.peers.GetUserPeers(ctx, user.Identifier) // ignore error, list will be empty in error case
|
||||||
|
|
||||||
|
user.LinkedPeerCount = len(peers)
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m Manager) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
func (m Manager) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -193,7 +212,7 @@ func (m Manager) CreateUser(ctx context.Context, user *domain.User) (*domain.Use
|
||||||
return nil, fmt.Errorf("unable to load existing user %s: %w", user.Identifier, err)
|
return nil, fmt.Errorf("unable to load existing user %s: %w", user.Identifier, err)
|
||||||
}
|
}
|
||||||
if existingUser != nil {
|
if existingUser != nil {
|
||||||
return nil, fmt.Errorf("user %s already exists", user.Identifier)
|
return nil, errors.Join(fmt.Errorf("user %s already exists", user.Identifier), domain.ErrDuplicateEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.validateCreation(ctx, user); err != nil {
|
if err := m.validateCreation(ctx, user); err != nil {
|
||||||
|
|
@ -240,6 +259,59 @@ func (m Manager) DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Manager) ActivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||||
|
user, err := m.users.GetUser(ctx, id)
|
||||||
|
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||||
|
return nil, fmt.Errorf("unable to find user %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.validateApiChange(ctx, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
user.ApiToken = uuid.New().String()
|
||||||
|
user.ApiTokenCreated = &now
|
||||||
|
|
||||||
|
err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||||
|
user.CopyCalculatedAttributes(u)
|
||||||
|
return user, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("update failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.bus.Publish(app.TopicUserApiEnabled, user)
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Manager) DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||||
|
user, err := m.users.GetUser(ctx, id)
|
||||||
|
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||||
|
return nil, fmt.Errorf("unable to find user %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.validateApiChange(ctx, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.ApiToken = ""
|
||||||
|
user.ApiTokenCreated = nil
|
||||||
|
|
||||||
|
err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||||
|
user.CopyCalculatedAttributes(u)
|
||||||
|
return user, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("update failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.bus.Publish(app.TopicUserApiDisabled, user)
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m Manager) validateModifications(ctx context.Context, old, new *domain.User) error {
|
func (m Manager) validateModifications(ctx context.Context, old, new *domain.User) error {
|
||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
|
|
@ -248,27 +320,27 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := old.EditAllowed(new); err != nil {
|
if err := old.EditAllowed(new); err != nil {
|
||||||
return fmt.Errorf("no access: %w", err)
|
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := old.CanChangePassword(); err != nil && string(new.Password) != "" {
|
if err := old.CanChangePassword(); err != nil && string(new.Password) != "" {
|
||||||
return fmt.Errorf("no access: %w", err)
|
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentUser.Id == old.Identifier && old.IsAdmin && !new.IsAdmin {
|
if currentUser.Id == old.Identifier && old.IsAdmin && !new.IsAdmin {
|
||||||
return fmt.Errorf("cannot remove own admin rights")
|
return fmt.Errorf("cannot remove own admin rights: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentUser.Id == old.Identifier && new.IsDisabled() {
|
if currentUser.Id == old.Identifier && new.IsDisabled() {
|
||||||
return fmt.Errorf("cannot disable own user")
|
return fmt.Errorf("cannot disable own user: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentUser.Id == old.Identifier && new.IsLocked() {
|
if currentUser.Id == old.Identifier && new.IsLocked() {
|
||||||
return fmt.Errorf("cannot lock own user")
|
return fmt.Errorf("cannot lock own user: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if old.Source != new.Source {
|
if old.Source != new.Source {
|
||||||
return fmt.Errorf("cannot change user source")
|
return fmt.Errorf("cannot change user source: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -282,19 +354,32 @@ func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if new.Identifier == "" {
|
if new.Identifier == "" {
|
||||||
return fmt.Errorf("invalid user identifier")
|
return fmt.Errorf("invalid user identifier: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if new.Identifier == "all" { // the all user identifier collides with the rest api routes
|
if new.Identifier == "all" { // the 'all' user identifier collides with the rest api routes
|
||||||
return fmt.Errorf("reserved user identifier")
|
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if new.Identifier == "new" { // the 'new' user identifier collides with the rest api routes
|
||||||
|
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if new.Identifier == "id" { // the 'id' user identifier collides with the rest api routes
|
||||||
|
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if new.Identifier == domain.CtxSystemAdminId || new.Identifier == domain.CtxUnknownUserId {
|
||||||
|
return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if new.Source != domain.UserSourceDatabase {
|
if new.Source != domain.UserSourceDatabase {
|
||||||
return fmt.Errorf("invalid user source: %s", new.Source)
|
return fmt.Errorf("invalid user source: %s, only %s is allowed: %w",
|
||||||
|
new.Source, domain.UserSourceDatabase, domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if string(new.Password) == "" {
|
if string(new.Password) == "" {
|
||||||
return fmt.Errorf("invalid password")
|
return fmt.Errorf("invalid password: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -304,15 +389,25 @@ func (m Manager) validateDeletion(ctx context.Context, del *domain.User) error {
|
||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin {
|
||||||
return fmt.Errorf("insufficient permissions")
|
return domain.ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := del.DeleteAllowed(); err != nil {
|
if err := del.DeleteAllowed(); err != nil {
|
||||||
return fmt.Errorf("no access: %w", err)
|
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentUser.Id == del.Identifier {
|
if currentUser.Id == del.Identifier {
|
||||||
return fmt.Errorf("cannot delete own user")
|
return fmt.Errorf("cannot delete own user: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Manager) validateApiChange(ctx context.Context, user *domain.User) error {
|
||||||
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
|
if currentUser.Id != user.Identifier {
|
||||||
|
return fmt.Errorf("cannot change API access of user: %w", domain.ErrNoPermission)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -357,7 +357,7 @@ func (m Manager) CreateInterface(ctx context.Context, in *domain.Interface) (*do
|
||||||
return nil, fmt.Errorf("unable to load existing interface %s: %w", in.Identifier, err)
|
return nil, fmt.Errorf("unable to load existing interface %s: %w", in.Identifier, err)
|
||||||
}
|
}
|
||||||
if existingInterface != nil {
|
if existingInterface != nil {
|
||||||
return nil, fmt.Errorf("interface %s already exists", in.Identifier)
|
return nil, fmt.Errorf("interface %s already exists: %w", in.Identifier, domain.ErrDuplicateEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.validateInterfaceCreation(ctx, existingInterface, in); err != nil {
|
if err := m.validateInterfaceCreation(ctx, existingInterface, in); err != nil {
|
||||||
|
|
@ -825,6 +825,13 @@ func (m Manager) validateInterfaceCreation(ctx context.Context, old, new *domain
|
||||||
return fmt.Errorf("insufficient permissions")
|
return fmt.Errorf("insufficient permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate public key if it is set
|
||||||
|
if new.PublicKey != "" && new.PrivateKey != "" {
|
||||||
|
if domain.PublicKeyFromPrivateKey(new.PrivateKey) != new.PublicKey {
|
||||||
|
return fmt.Errorf("invalid public key for given privatekey: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
@ -34,9 +33,9 @@ func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdenti
|
||||||
}
|
}
|
||||||
|
|
||||||
peer.UserIdentifier = userId
|
peer.UserIdentifier = userId
|
||||||
peer.DisplayName = fmt.Sprintf("Default Peer %s", internal.TruncateString(string(peer.Identifier), 8))
|
|
||||||
peer.Notes = fmt.Sprintf("Default peer created for user %s", userId)
|
peer.Notes = fmt.Sprintf("Default peer created for user %s", userId)
|
||||||
peer.AutomaticallyCreated = true
|
peer.AutomaticallyCreated = true
|
||||||
|
peer.GenerateDisplayName("Default")
|
||||||
|
|
||||||
newPeers = append(newPeers, *peer)
|
newPeers = append(newPeers, *peer)
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +107,6 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
|
||||||
ExtraAllowedIPsStr: "",
|
ExtraAllowedIPsStr: "",
|
||||||
PresharedKey: pk,
|
PresharedKey: pk,
|
||||||
PersistentKeepalive: domain.NewConfigOption(iface.PeerDefPersistentKeepalive, true),
|
PersistentKeepalive: domain.NewConfigOption(iface.PeerDefPersistentKeepalive, true),
|
||||||
DisplayName: fmt.Sprintf("Peer %s", internal.TruncateString(string(peerId), 8)),
|
|
||||||
Identifier: peerId,
|
Identifier: peerId,
|
||||||
UserIdentifier: currentUser.Id,
|
UserIdentifier: currentUser.Id,
|
||||||
InterfaceIdentifier: iface.Identifier,
|
InterfaceIdentifier: iface.Identifier,
|
||||||
|
|
@ -132,6 +130,7 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
|
||||||
PostDown: domain.NewConfigOption(iface.PeerDefPostDown, true),
|
PostDown: domain.NewConfigOption(iface.PeerDefPostDown, true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
freshPeer.GenerateDisplayName("")
|
||||||
|
|
||||||
return freshPeer, nil
|
return freshPeer, nil
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +158,7 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
|
||||||
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
|
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
|
||||||
}
|
}
|
||||||
if existingPeer != nil {
|
if existingPeer != nil {
|
||||||
return nil, fmt.Errorf("peer %s already exists", peer.Identifier)
|
return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil {
|
if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil {
|
||||||
|
|
@ -234,6 +233,15 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
|
||||||
if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
|
if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
|
||||||
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier
|
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier
|
||||||
|
|
||||||
|
// check for already existing peer with new identifier
|
||||||
|
duplicatePeer, err := m.db.GetPeer(ctx, peer.Identifier)
|
||||||
|
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||||
|
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
|
||||||
|
}
|
||||||
|
if duplicatePeer != nil {
|
||||||
|
return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
|
||||||
|
}
|
||||||
|
|
||||||
// delete old peer
|
// delete old peer
|
||||||
err = m.DeletePeer(ctx, existingPeer.Identifier)
|
err = m.DeletePeer(ctx, existingPeer.Identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -431,7 +439,7 @@ func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain
|
||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin {
|
||||||
return fmt.Errorf("insufficient permissions")
|
return domain.ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -441,11 +449,16 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
|
||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if new.Identifier == "" {
|
if new.Identifier == "" {
|
||||||
return fmt.Errorf("invalid peer identifier")
|
return fmt.Errorf("invalid peer identifier: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin {
|
||||||
return fmt.Errorf("insufficient permissions")
|
return domain.ErrNoPermission
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := m.db.GetInterface(ctx, new.InterfaceIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid interface: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -455,7 +468,7 @@ func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) err
|
||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin {
|
||||||
return fmt.Errorf("insufficient permissions")
|
return domain.ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ type Config struct {
|
||||||
ExpiryCheckInterval time.Duration `yaml:"expiry_check_interval"`
|
ExpiryCheckInterval time.Duration `yaml:"expiry_check_interval"`
|
||||||
RulePrioOffset int `yaml:"rule_prio_offset"`
|
RulePrioOffset int `yaml:"rule_prio_offset"`
|
||||||
RouteTableOffset int `yaml:"route_table_offset"`
|
RouteTableOffset int `yaml:"route_table_offset"`
|
||||||
|
ApiAdminOnly bool `yaml:"api_admin_only"` // if true, only admin users can access the API
|
||||||
} `yaml:"advanced"`
|
} `yaml:"advanced"`
|
||||||
|
|
||||||
Statistics struct {
|
Statistics struct {
|
||||||
|
|
@ -126,6 +127,7 @@ func defaultConfig() *Config {
|
||||||
cfg.Advanced.ExpiryCheckInterval = 15 * time.Minute
|
cfg.Advanced.ExpiryCheckInterval = 15 * time.Minute
|
||||||
cfg.Advanced.RulePrioOffset = 20000
|
cfg.Advanced.RulePrioOffset = 20000
|
||||||
cfg.Advanced.RouteTableOffset = 20000
|
cfg.Advanced.RouteTableOffset = 20000
|
||||||
|
cfg.Advanced.ApiAdminOnly = true
|
||||||
|
|
||||||
cfg.Statistics.UsePingChecks = true
|
cfg.Statistics.UsePingChecks = true
|
||||||
cfg.Statistics.PingCheckWorkers = 10
|
cfg.Statistics.PingCheckWorkers = 10
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package domain
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
@ -28,6 +29,7 @@ func (u *ContextUserInfo) UserId() string {
|
||||||
return string(u.Id)
|
return string(u.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultContextUserInfo returns a default context user info.
|
||||||
func DefaultContextUserInfo() *ContextUserInfo {
|
func DefaultContextUserInfo() *ContextUserInfo {
|
||||||
return &ContextUserInfo{
|
return &ContextUserInfo{
|
||||||
Id: CtxUnknownUserId,
|
Id: CtxUnknownUserId,
|
||||||
|
|
@ -35,6 +37,7 @@ func DefaultContextUserInfo() *ContextUserInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SystemAdminContextUserInfo returns a context user info for the system admin.
|
||||||
func SystemAdminContextUserInfo() *ContextUserInfo {
|
func SystemAdminContextUserInfo() *ContextUserInfo {
|
||||||
return &ContextUserInfo{
|
return &ContextUserInfo{
|
||||||
Id: CtxSystemAdminId,
|
Id: CtxSystemAdminId,
|
||||||
|
|
@ -42,6 +45,7 @@ func SystemAdminContextUserInfo() *ContextUserInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserInfoFromGin sets the user info from the gin context to the request context.
|
||||||
func SetUserInfoFromGin(c *gin.Context) context.Context {
|
func SetUserInfoFromGin(c *gin.Context) context.Context {
|
||||||
ginUserInfo, exists := c.Get(CtxUserInfo)
|
ginUserInfo, exists := c.Get(CtxUserInfo)
|
||||||
|
|
||||||
|
|
@ -56,11 +60,13 @@ func SetUserInfoFromGin(c *gin.Context) context.Context {
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetUserInfo sets the user info in the context.
|
||||||
func SetUserInfo(ctx context.Context, info *ContextUserInfo) context.Context {
|
func SetUserInfo(ctx context.Context, info *ContextUserInfo) context.Context {
|
||||||
ctx = context.WithValue(ctx, CtxUserInfo, info)
|
ctx = context.WithValue(ctx, CtxUserInfo, info)
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserInfo returns the user info from the context.
|
||||||
func GetUserInfo(ctx context.Context) *ContextUserInfo {
|
func GetUserInfo(ctx context.Context) *ContextUserInfo {
|
||||||
rawInfo := ctx.Value(CtxUserInfo)
|
rawInfo := ctx.Value(CtxUserInfo)
|
||||||
if rawInfo == nil {
|
if rawInfo == nil {
|
||||||
|
|
@ -74,6 +80,8 @@ func GetUserInfo(ctx context.Context) *ContextUserInfo {
|
||||||
return DefaultContextUserInfo()
|
return DefaultContextUserInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateUserAccessRights checks if the current user has access rights to the requested user.
|
||||||
|
// If the user is an admin, access is granted.
|
||||||
func ValidateUserAccessRights(ctx context.Context, requiredUser UserIdentifier) error {
|
func ValidateUserAccessRights(ctx context.Context, requiredUser UserIdentifier) error {
|
||||||
sessionUser := GetUserInfo(ctx)
|
sessionUser := GetUserInfo(ctx)
|
||||||
|
|
||||||
|
|
@ -86,9 +94,10 @@ func ValidateUserAccessRights(ctx context.Context, requiredUser UserIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Warnf("insufficient permissions for %s (want %s), stack: %s", sessionUser.Id, requiredUser, GetStackTrace())
|
logrus.Warnf("insufficient permissions for %s (want %s), stack: %s", sessionUser.Id, requiredUser, GetStackTrace())
|
||||||
return fmt.Errorf("insufficient permissions")
|
return ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateAdminAccessRights checks if the current user has admin access rights.
|
||||||
func ValidateAdminAccessRights(ctx context.Context) error {
|
func ValidateAdminAccessRights(ctx context.Context) error {
|
||||||
sessionUser := GetUserInfo(ctx)
|
sessionUser := GetUserInfo(ctx)
|
||||||
|
|
||||||
|
|
@ -97,5 +106,5 @@ func ValidateAdminAccessRights(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Warnf("insufficient admin permissions for %s, stack: %s", sessionUser.Id, GetStackTrace())
|
logrus.Warnf("insufficient admin permissions for %s, stack: %s", sessionUser.Id, GetStackTrace())
|
||||||
return fmt.Errorf("insufficient permissions")
|
return ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ import (
|
||||||
|
|
||||||
var ErrNotFound = errors.New("record not found")
|
var ErrNotFound = errors.New("record not found")
|
||||||
var ErrNotUnique = errors.New("record not unique")
|
var ErrNotUnique = errors.New("record not unique")
|
||||||
|
var ErrNoPermission = errors.New("no permission")
|
||||||
|
var ErrDuplicateEntry = errors.New("duplicate entry")
|
||||||
|
var ErrInvalidData = errors.New("invalid data")
|
||||||
|
|
||||||
// GetStackTrace returns a stack trace of the current goroutine. The stack trace has at most 1024 bytes.
|
// GetStackTrace returns a stack trace of the current goroutine. The stack trace has at most 1024 bytes.
|
||||||
func GetStackTrace() string {
|
func GetStackTrace() string {
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,13 @@ func (p *Peer) ApplyInterfaceDefaults(in *Interface) {
|
||||||
p.Interface.PostDown.TrySetValue(in.PeerDefPostDown)
|
p.Interface.PostDown.TrySetValue(in.PeerDefPostDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Peer) GenerateDisplayName(prefix string) {
|
||||||
|
if prefix != "" {
|
||||||
|
prefix = fmt.Sprintf("%s ", strings.TrimSpace(prefix)) // add a space after the prefix
|
||||||
|
}
|
||||||
|
p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8))
|
||||||
|
}
|
||||||
|
|
||||||
type PeerInterfaceConfig struct {
|
type PeerInterfaceConfig struct {
|
||||||
KeyPair // private/public Key of the peer
|
KeyPair // private/public Key of the peer
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -42,6 +43,10 @@ type User struct {
|
||||||
Locked *time.Time `gorm:"index;column:locked"` // if this field is set, the user is locked and can no longer login (WireGuard peers still can connect)
|
Locked *time.Time `gorm:"index;column:locked"` // if this field is set, the user is locked and can no longer login (WireGuard peers still can connect)
|
||||||
LockedReason string // the reason why the user has been locked
|
LockedReason string // the reason why the user has been locked
|
||||||
|
|
||||||
|
// API token for REST API access
|
||||||
|
ApiToken string `form:"api_token" binding:"omitempty"`
|
||||||
|
ApiTokenCreated *time.Time
|
||||||
|
|
||||||
LinkedPeerCount int `gorm:"-"`
|
LinkedPeerCount int `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,6 +61,14 @@ func (u *User) IsLocked() bool {
|
||||||
return u.Locked != nil
|
return u.Locked != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) IsApiEnabled() bool {
|
||||||
|
if u.ApiToken != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) CanChangePassword() error {
|
func (u *User) CanChangePassword() error {
|
||||||
if u.Source == UserSourceDatabase {
|
if u.Source == UserSourceDatabase {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -115,6 +128,18 @@ func (u *User) CheckPassword(password string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) CheckApiToken(token string) error {
|
||||||
|
if !u.IsApiEnabled() {
|
||||||
|
return errors.New("api access disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if res := subtle.ConstantTimeCompare([]byte(u.ApiToken), []byte(token)); res != 1 {
|
||||||
|
return errors.New("wrong token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) HashPassword() error {
|
func (u *User) HashPassword() error {
|
||||||
if u.Password == "" {
|
if u.Password == "" {
|
||||||
return nil // nothing to hash
|
return nil // nothing to hash
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue