mirror of
https://github.com/h44z/wg-portal.git
synced 2025-04-19 08:55:12 +00:00
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
|
||||||
|
77
frontend/src/views/SettingsView.vue
Normal file
77
frontend/src/views/SettingsView.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<script setup>
|
||||||
|
import PeerViewModal from "../components/PeerViewModal.vue";
|
||||||
|
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { profileStore } from "@/stores/profile";
|
||||||
|
import PeerEditModal from "@/components/PeerEditModal.vue";
|
||||||
|
import { settingsStore } from "@/stores/settings";
|
||||||
|
import { humanFileSize } from "@/helpers/utils";
|
||||||
|
import {RouterLink} from "vue-router";
|
||||||
|
import {authStore} from "../stores/auth";
|
||||||
|
|
||||||
|
const profile = profileStore()
|
||||||
|
const settings = settingsStore()
|
||||||
|
const auth = authStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await profile.LoadUser()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>{{ $t('settings.headline') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="lead">{{ $t('settings.abstract') }}</p>
|
||||||
|
|
||||||
|
<div v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')">
|
||||||
|
<div class="bg-light p-5" v-if="profile.user.ApiToken">
|
||||||
|
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||||
|
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
<p>{{ $t('settings.api.active-description') }}</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('settings.api.user-label') }}</label>
|
||||||
|
<input v-model="profile.user.Identifier" class="form-control" :placeholder="$t('settings.api.user-placeholder')" type="text" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('settings.api.token-label') }}</label>
|
||||||
|
<input v-model="profile.user.ApiToken" class="form-control" :placeholder="$t('settings.api.token-placeholder')" type="text" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<p class="form-label mt-4">{{ $t('settings.api.token-created-label') }} {{profile.user.ApiTokenCreated}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col-6">
|
||||||
|
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-disable-title')" @click.prevent="profile.disableApi()" :disabled="profile.isFetching">
|
||||||
|
<i class="fa-solid fa-minus-circle"></i> {{ $t('settings.api.button-disable-text') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<a href="/api/v1/doc.html" target="_blank" :alt="$t('settings.api.api-link')">{{ $t('settings.api.api-link') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-light p-5" v-else>
|
||||||
|
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||||
|
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
<p>{{ $t('settings.api.inactive-description') }}</p>
|
||||||
|
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-enable-title')" @click.prevent="profile.enableApi()" :disabled="profile.isFetching">
|
||||||
|
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.api.button-enable-text') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -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
|
||||||
|
1932
internal/app/api/core/assets/doc/v1_swagger.json
Normal file
1932
internal/app/api/core/assets/doc/v1_swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
1358
internal/app/api/core/assets/doc/v1_swagger.yaml
Normal file
1358
internal/app/api/core/assets/doc/v1_swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
3917
internal/app/api/core/assets/js/rapidoc-min.js
vendored
3917
internal/app/api/core/assets/js/rapidoc-min.js
vendored
File diff suppressed because one or more lines are too long
@ -8,10 +8,16 @@
|
|||||||
<rapi-doc
|
<rapi-doc
|
||||||
spec-url="{{ $.ApiSpecUrl }}"
|
spec-url="{{ $.ApiSpecUrl }}"
|
||||||
theme="dark"
|
theme="dark"
|
||||||
|
render-style="focused"
|
||||||
allow-server-selection="false"
|
allow-server-selection="false"
|
||||||
allow-authentication="false"
|
allow-authentication="true"
|
||||||
load-fonts="false"
|
load-fonts="false"
|
||||||
|
schema-style="table"
|
||||||
schema-expand-level="1"
|
schema-expand-level="1"
|
||||||
|
default-schema-tab="model"
|
||||||
|
fill-request-fields-with-example="true"
|
||||||
|
show-method-in-nav-bar="as-colored-block"
|
||||||
|
show-components="true"
|
||||||
allow-spec-url-load="false"
|
allow-spec-url-load="false"
|
||||||
allow-spec-file-load="false"
|
allow-spec-file-load="false"
|
||||||
allow-spec-file-download="true"
|
allow-spec-file-download="true"
|
||||||
|
@ -88,6 +88,8 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server,
|
|||||||
s.server.StaticFS("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
s.server.StaticFS("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes
|
||||||
|
s.server.UseRawPath = true
|
||||||
|
s.server.UnescapePathValues = true
|
||||||
s.setupRoutes(endpoints...)
|
s.setupRoutes(endpoints...)
|
||||||
s.setupFrontendRoutes()
|
s.setupFrontendRoutes()
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-contrib/sessions/memstore"
|
"github.com/gin-contrib/sessions/memstore"
|
||||||
@ -10,8 +13,6 @@ import (
|
|||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
csrf "github.com/utrack/gin-csrf"
|
csrf "github.com/utrack/gin-csrf"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type handler interface {
|
type handler interface {
|
||||||
@ -20,12 +21,12 @@ type handler interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// To compile the API documentation use the
|
// To compile the API documentation use the
|
||||||
// build_tool
|
// api_build_tool
|
||||||
// command that can be found in the $PROJECT_ROOT/internal/ports/api/build_tool directory.
|
// command that can be found in the $PROJECT_ROOT/cmd/api_build_tool directory.
|
||||||
|
|
||||||
// @title WireGuard Portal API
|
// @title WireGuard Portal SPA-UI API
|
||||||
// @version 0.0
|
// @version 0.0
|
||||||
// @description WireGuard Portal API - a testing API endpoint
|
// @description WireGuard Portal API - UI Endpoints
|
||||||
|
|
||||||
// @contact.name WireGuard Portal Developers
|
// @contact.name WireGuard Portal Developers
|
||||||
// @contact.url https://github.com/h44z/wg-portal
|
// @contact.url https://github.com/h44z/wg-portal
|
||||||
|
@ -4,13 +4,14 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
|
||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed frontend_config.js.gotpl
|
//go:embed frontend_config.js.gotpl
|
||||||
@ -63,7 +64,8 @@ func (e configEndpoint) handleConfigJsGet() gin.HandlerFunc {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
||||||
}
|
}
|
||||||
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host, port) // override if request comes from frontend started with npm run dev
|
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host,
|
||||||
|
port) // override if request comes from frontend started with npm run dev
|
||||||
}
|
}
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
err := e.tpl.ExecuteTemplate(buf, "frontend_config.js.gotpl", gin.H{
|
err := e.tpl.ExecuteTemplate(buf, "frontend_config.js.gotpl", gin.H{
|
||||||
@ -96,6 +98,7 @@ func (e configEndpoint) handleSettingsGet() gin.HandlerFunc {
|
|||||||
MailLinkOnly: e.app.Config.Mail.LinkOnly,
|
MailLinkOnly: e.app.Config.Mail.LinkOnly,
|
||||||
PersistentConfigSupported: e.app.Config.Advanced.ConfigStoragePath != "",
|
PersistentConfigSupported: e.app.Config.Advanced.ConfigStoragePath != "",
|
||||||
SelfProvisioning: e.app.Config.Core.SelfProvisioningAllowed,
|
SelfProvisioning: e.app.Config.Core.SelfProvisioningAllowed,
|
||||||
|
ApiAdminOnly: e.app.Config.Advanced.ApiAdminOnly,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type peerEndpoint struct {
|
type peerEndpoint struct {
|
||||||
@ -57,7 +58,8 @@ func (e peerEndpoint) handleAllGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
_, peers, err := e.app.GetInterfaceAndPeers(ctx, domain.InterfaceIdentifier(interfaceId))
|
_, peers, err := e.app.GetInterfaceAndPeers(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +90,8 @@ func (e peerEndpoint) handleSingleGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
peer, err := e.app.GetPeer(ctx, domain.PeerIdentifier(peerId))
|
peer, err := e.app.GetPeer(ctx, domain.PeerIdentifier(peerId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +122,8 @@ func (e peerEndpoint) handlePrepareGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
peer, err := e.app.PreparePeer(ctx, domain.InterfaceIdentifier(interfaceId))
|
peer, err := e.app.PreparePeer(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +167,8 @@ func (e peerEndpoint) handleCreatePost() gin.HandlerFunc {
|
|||||||
|
|
||||||
newPeer, err := e.app.CreatePeer(ctx, model.NewDomainPeer(&p))
|
newPeer, err := e.app.CreatePeer(ctx, model.NewDomainPeer(&p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,9 +205,11 @@ func (e peerEndpoint) handleCreateMultiplePost() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newPeers, err := e.app.CreateMultiplePeers(ctx, domain.InterfaceIdentifier(interfaceId), model.NewDomainPeerCreationRequest(&req))
|
newPeers, err := e.app.CreateMultiplePeers(ctx, domain.InterfaceIdentifier(interfaceId),
|
||||||
|
model.NewDomainPeerCreationRequest(&req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,7 +253,8 @@ func (e peerEndpoint) handleUpdatePut() gin.HandlerFunc {
|
|||||||
|
|
||||||
updatedPeer, err := e.app.UpdatePeer(ctx, model.NewDomainPeer(&p))
|
updatedPeer, err := e.app.UpdatePeer(ctx, model.NewDomainPeer(&p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +285,8 @@ func (e peerEndpoint) handleDelete() gin.HandlerFunc {
|
|||||||
|
|
||||||
err := e.app.DeletePeer(ctx, domain.PeerIdentifier(id))
|
err := e.app.DeletePeer(ctx, domain.PeerIdentifier(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,9 +342,10 @@ func (e peerEndpoint) handleConfigGet() gin.HandlerFunc {
|
|||||||
// @ID peers_handleQrCodeGet
|
// @ID peers_handleQrCodeGet
|
||||||
// @Tags Peer
|
// @Tags Peer
|
||||||
// @Summary Get peer configuration as qr code.
|
// @Summary Get peer configuration as qr code.
|
||||||
|
// @Produce png
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "The peer identifier"
|
// @Param id path string true "The peer identifier"
|
||||||
// @Success 200 {object} string
|
// @Success 200 {file} binary
|
||||||
// @Failure 400 {object} model.Error
|
// @Failure 400 {object} model.Error
|
||||||
// @Failure 500 {object} model.Error
|
// @Failure 500 {object} model.Error
|
||||||
// @Router /peer/config-qr/{id} [get]
|
// @Router /peer/config-qr/{id} [get]
|
||||||
@ -403,7 +413,8 @@ func (e peerEndpoint) handleEmailPost() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
err = e.app.SendPeerEmail(ctx, req.LinkOnly, peerIds...)
|
err = e.app.SendPeerEmail(ctx, req.LinkOnly, peerIds...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,7 +445,8 @@ func (e peerEndpoint) handleStatsGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
stats, err := e.app.GetPeerStats(ctx, domain.InterfaceIdentifier(interfaceId))
|
stats, err := e.app.GetPeerStats(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type userEndpoint struct {
|
type userEndpoint struct {
|
||||||
@ -27,6 +28,8 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
|
|||||||
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
|
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
|
||||||
apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
|
apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
|
||||||
|
apiGroup.POST("/:id/api/enable", e.authenticator.UserIdMatch("id"), e.handleApiEnablePost())
|
||||||
|
apiGroup.POST("/:id/api/disable", e.authenticator.UserIdMatch("id"), e.handleApiDisablePost())
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAllGet returns a gorm handler function.
|
// handleAllGet returns a gorm handler function.
|
||||||
@ -44,7 +47,8 @@ func (e userEndpoint) handleAllGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
users, err := e.app.GetAllUsers(ctx)
|
users, err := e.app.GetAllUsers(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,11 +78,12 @@ func (e userEndpoint) handleSingleGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
user, err := e.app.GetUser(ctx, domain.UserIdentifier(id))
|
user, err := e.app.GetUser(ctx, domain.UserIdentifier(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewUser(user))
|
c.JSON(http.StatusOK, model.NewUser(user, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,11 +123,12 @@ func (e userEndpoint) handleUpdatePut() gin.HandlerFunc {
|
|||||||
|
|
||||||
updateUser, err := e.app.UpdateUser(ctx, model.NewDomainUser(&user))
|
updateUser, err := e.app.UpdateUser(ctx, model.NewDomainUser(&user))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewUser(updateUser))
|
c.JSON(http.StatusOK, model.NewUser(updateUser, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,11 +156,12 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
|
|||||||
|
|
||||||
newUser, err := e.app.CreateUser(ctx, model.NewDomainUser(&user))
|
newUser, err := e.app.CreateUser(ctx, model.NewDomainUser(&user))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewUser(newUser))
|
c.JSON(http.StatusOK, model.NewUser(newUser, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,13 +181,15 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
interfaceId := Base64UrlDecode(c.Param("id"))
|
interfaceId := Base64UrlDecode(c.Param("id"))
|
||||||
if interfaceId == "" {
|
if interfaceId == "" {
|
||||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(interfaceId))
|
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(interfaceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,13 +213,15 @@ func (e userEndpoint) handleStatsGet() gin.HandlerFunc {
|
|||||||
|
|
||||||
userId := Base64UrlDecode(c.Param("id"))
|
userId := Base64UrlDecode(c.Param("id"))
|
||||||
if userId == "" {
|
if userId == "" {
|
||||||
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stats, err := e.app.GetUserPeerStats(ctx, domain.UserIdentifier(userId))
|
stats, err := e.app.GetUserPeerStats(ctx, domain.UserIdentifier(userId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,10 +252,75 @@ func (e userEndpoint) handleDelete() gin.HandlerFunc {
|
|||||||
|
|
||||||
err := e.app.DeleteUser(ctx, domain.UserIdentifier(id))
|
err := e.app.DeleteUser(ctx, domain.UserIdentifier(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleApiEnablePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleApiEnablePost
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Enable the REST API for the given user.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} model.User
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/{id}/api/enable [post]
|
||||||
|
func (e userEndpoint) handleApiEnablePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
userId := Base64UrlDecode(c.Param("id"))
|
||||||
|
if userId == "" {
|
||||||
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := e.app.ActivateApi(ctx, domain.UserIdentifier(userId))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.NewUser(user, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleApiDisablePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleApiDisablePost
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Disable the REST API for the given user.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} model.User
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/{id}/api/disable [post]
|
||||||
|
func (e userEndpoint) handleApiDisablePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
userId := Base64UrlDecode(c.Param("id"))
|
||||||
|
if userId == "" {
|
||||||
|
c.JSON(http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := e.app.DeactivateApi(ctx, domain.UserIdentifier(userId))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.NewUser(user, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,4 +9,5 @@ type Settings struct {
|
|||||||
MailLinkOnly bool `json:"MailLinkOnly"`
|
MailLinkOnly bool `json:"MailLinkOnly"`
|
||||||
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
||||||
SelfProvisioning bool `json:"SelfProvisioning"`
|
SelfProvisioning bool `json:"SelfProvisioning"`
|
||||||
|
ApiAdminOnly bool `json:"ApiAdminOnly"`
|
||||||
}
|
}
|
||||||
|
@ -25,37 +25,50 @@ type User struct {
|
|||||||
Locked bool `json:"Locked"` // if this field is set, the user is locked
|
Locked bool `json:"Locked"` // if this field is set, the user is locked
|
||||||
LockedReason string `json:"LockedReason"` // the reason why the user has been locked
|
LockedReason string `json:"LockedReason"` // the reason why the user has been locked
|
||||||
|
|
||||||
|
ApiToken string `json:"ApiToken"`
|
||||||
|
ApiTokenCreated *time.Time `json:"ApiTokenCreated,omitempty"`
|
||||||
|
ApiEnabled bool `json:"ApiEnabled"`
|
||||||
|
|
||||||
// Calculated
|
// Calculated
|
||||||
|
|
||||||
PeerCount int `json:"PeerCount"`
|
PeerCount int `json:"PeerCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser(src *domain.User) *User {
|
func NewUser(src *domain.User, exposeCreds bool) *User {
|
||||||
return &User{
|
u := &User{
|
||||||
Identifier: string(src.Identifier),
|
Identifier: string(src.Identifier),
|
||||||
Email: src.Email,
|
Email: src.Email,
|
||||||
Source: string(src.Source),
|
Source: string(src.Source),
|
||||||
ProviderName: src.ProviderName,
|
ProviderName: src.ProviderName,
|
||||||
IsAdmin: src.IsAdmin,
|
IsAdmin: src.IsAdmin,
|
||||||
Firstname: src.Firstname,
|
Firstname: src.Firstname,
|
||||||
Lastname: src.Lastname,
|
Lastname: src.Lastname,
|
||||||
Phone: src.Phone,
|
Phone: src.Phone,
|
||||||
Department: src.Department,
|
Department: src.Department,
|
||||||
Notes: src.Notes,
|
Notes: src.Notes,
|
||||||
Password: "", // never fill password
|
Password: "", // never fill password
|
||||||
Disabled: src.IsDisabled(),
|
Disabled: src.IsDisabled(),
|
||||||
DisabledReason: src.DisabledReason,
|
DisabledReason: src.DisabledReason,
|
||||||
Locked: src.IsLocked(),
|
Locked: src.IsLocked(),
|
||||||
LockedReason: src.LockedReason,
|
LockedReason: src.LockedReason,
|
||||||
|
ApiToken: "", // by default, do not expose API token
|
||||||
|
ApiTokenCreated: src.ApiTokenCreated,
|
||||||
|
ApiEnabled: src.IsApiEnabled(),
|
||||||
|
|
||||||
PeerCount: src.LinkedPeerCount,
|
PeerCount: src.LinkedPeerCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if exposeCreds {
|
||||||
|
u.ApiToken = src.ApiToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUsers(src []domain.User) []User {
|
func NewUsers(src []domain.User) []User {
|
||||||
results := make([]User, len(src))
|
results := make([]User, len(src))
|
||||||
for i := range src {
|
for i := range src {
|
||||||
results[i] = *NewUser(&src[i])
|
results[i] = *NewUser(&src[i], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
109
internal/app/api/v1/backend/interface_service.go
Normal file
109
internal/app/api/v1/backend/interface_service.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InterfaceServiceInterfaceManagerRepo interface {
|
||||||
|
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||||
|
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
|
||||||
|
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||||
|
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
interfaces InterfaceServiceInterfaceManagerRepo
|
||||||
|
users PeerServiceUserManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterfaceService(cfg *config.Config, interfaces InterfaceServiceInterfaceManagerRepo) *InterfaceService {
|
||||||
|
return &InterfaceService{
|
||||||
|
cfg: cfg,
|
||||||
|
interfaces: interfaces,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) GetAll(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces, interfacePeers, err := s.interfaces.GetAllInterfacesAndPeers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces, interfacePeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) GetById(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
|
*domain.Interface,
|
||||||
|
[]domain.Peer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceData, interfacePeers, err := s.interfaces.GetInterfaceAndPeers(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaceData, interfacePeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) Create(ctx context.Context, iface *domain.Interface) (*domain.Interface, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createdInterface, err := s.interfaces.CreateInterface(ctx, iface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdInterface, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) Update(ctx context.Context, id domain.InterfaceIdentifier, iface *domain.Interface) (
|
||||||
|
*domain.Interface,
|
||||||
|
[]domain.Peer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if iface.Identifier != id {
|
||||||
|
return nil, nil, fmt.Errorf("interface id mismatch: %s != %s: %w",
|
||||||
|
iface.Identifier, id, domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedInterface, updatedPeers, err := s.interfaces.UpdateInterface(ctx, iface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedInterface, updatedPeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) Delete(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.interfaces.DeleteInterface(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
143
internal/app/api/v1/backend/peer_service.go
Normal file
143
internal/app/api/v1/backend/peer_service.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PeerServicePeerManagerRepo interface {
|
||||||
|
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
|
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
|
||||||
|
UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
|
||||||
|
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerServiceUserManagerRepo interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
peers PeerServicePeerManagerRepo
|
||||||
|
users PeerServiceUserManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeerService(
|
||||||
|
cfg *config.Config,
|
||||||
|
peers PeerServicePeerManagerRepo,
|
||||||
|
users PeerServiceUserManagerRepo,
|
||||||
|
) *PeerService {
|
||||||
|
return &PeerService{
|
||||||
|
cfg: cfg,
|
||||||
|
peers: peers,
|
||||||
|
users: users,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) GetForInterface(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, interfacePeers, err := s.peers.GetInterfaceAndPeers(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfacePeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) GetForUser(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||||
|
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.users.GetUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userPeers, err := s.peers.GetUserPeers(ctx, user.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userPeers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) GetById(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
|
||||||
|
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||||
|
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := s.peers.GetPeer(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has access rights to the requested peer.
|
||||||
|
// If the peer is not linked to any user, access is granted only for admins.
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return peer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) Create(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
|
||||||
|
return nil, fmt.Errorf("peer id mismatch: %s != %s: %w",
|
||||||
|
peer.Identifier, peer.Interface.PublicKey, domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdPeer, err := s.peers.CreatePeer(ctx, peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdPeer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) Update(ctx context.Context, _ domain.PeerIdentifier, peer *domain.Peer) (
|
||||||
|
*domain.Peer,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedPeer, err := s.peers.UpdatePeer(ctx, peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedPeer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s PeerService) Delete(ctx context.Context, id domain.PeerIdentifier) error {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.peers.DeletePeer(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
174
internal/app/api/v1/backend/provisioning_service.go
Normal file
174
internal/app/api/v1/backend/provisioning_service.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProvisioningServiceUserManagerRepo interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningServicePeerManagerRepo interface {
|
||||||
|
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
GetUserPeers(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
|
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
|
||||||
|
CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningServiceConfigFileManagerRepo interface {
|
||||||
|
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
||||||
|
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
users ProvisioningServiceUserManagerRepo
|
||||||
|
peers ProvisioningServicePeerManagerRepo
|
||||||
|
configFiles ProvisioningServiceConfigFileManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvisioningService(
|
||||||
|
cfg *config.Config,
|
||||||
|
users ProvisioningServiceUserManagerRepo,
|
||||||
|
peers ProvisioningServicePeerManagerRepo,
|
||||||
|
configFiles ProvisioningServiceConfigFileManagerRepo,
|
||||||
|
) *ProvisioningService {
|
||||||
|
return &ProvisioningService{
|
||||||
|
cfg: cfg,
|
||||||
|
|
||||||
|
users: users,
|
||||||
|
peers: peers,
|
||||||
|
configFiles: configFiles,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) GetUserAndPeers(
|
||||||
|
ctx context.Context,
|
||||||
|
userId domain.UserIdentifier,
|
||||||
|
email string,
|
||||||
|
) (*domain.User, []domain.Peer, error) {
|
||||||
|
// first fetch user
|
||||||
|
var user *domain.User
|
||||||
|
switch {
|
||||||
|
case userId != "":
|
||||||
|
u, err := p.users.GetUser(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
user = u
|
||||||
|
case email != "":
|
||||||
|
u, err := p.users.GetUserByEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
user = u
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("either UserId or Email must be set: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, user.Identifier); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, err := p.peers.GetUserPeers(ctx, user.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, peers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) GetPeerConfig(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error) {
|
||||||
|
peer, err := p.peers.GetPeer(ctx, peerId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgData, err := io.ReadAll(peerCfgReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerCfgData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) GetPeerQrPng(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error) {
|
||||||
|
peer, err := p.peers.GetPeer(ctx, peerId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCfgQrData, err := io.ReadAll(peerCfgQrReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerCfgQrData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ProvisioningService) NewPeer(ctx context.Context, req models.ProvisioningRequest) (*domain.Peer, error) {
|
||||||
|
if req.UserIdentifier == "" {
|
||||||
|
req.UserIdentifier = string(domain.GetUserInfo(ctx).Id) // use authenticated user id if not set
|
||||||
|
}
|
||||||
|
|
||||||
|
// check permissions
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, domain.UserIdentifier(req.UserIdentifier)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !p.cfg.Core.SelfProvisioningAllowed {
|
||||||
|
// only admins can create new peers if self-provisioning is disabled
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare new peer
|
||||||
|
peer, err := p.peers.PreparePeer(ctx, domain.InterfaceIdentifier(req.InterfaceIdentifier))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to prepare new peer: %w", err)
|
||||||
|
}
|
||||||
|
peer.UserIdentifier = domain.UserIdentifier(req.UserIdentifier) // overwrite context user id with the one from the request
|
||||||
|
if req.PublicKey != "" {
|
||||||
|
peer.Identifier = domain.PeerIdentifier(req.PublicKey)
|
||||||
|
peer.Interface.PublicKey = req.PublicKey
|
||||||
|
peer.Interface.PrivateKey = "" // clear private key if public key is set, WireGuard Portal does not know the private key in that case
|
||||||
|
}
|
||||||
|
if req.PresharedKey != "" {
|
||||||
|
peer.PresharedKey = domain.PreSharedKey(req.PresharedKey)
|
||||||
|
}
|
||||||
|
peer.GenerateDisplayName("API")
|
||||||
|
|
||||||
|
// save new peer
|
||||||
|
peer, err = p.peers.CreatePeer(ctx, peer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new peer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return peer, nil
|
||||||
|
}
|
107
internal/app/api/v1/backend/user_service.go
Normal file
107
internal/app/api/v1/backend/user_service.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserManagerRepo interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||||
|
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
|
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
|
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserService struct {
|
||||||
|
cfg *config.Config
|
||||||
|
|
||||||
|
users UserManagerRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserService(cfg *config.Config, users UserManagerRepo) *UserService {
|
||||||
|
return &UserService{
|
||||||
|
cfg: cfg,
|
||||||
|
users: users,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) GetAll(ctx context.Context) ([]domain.User, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allUsers, err := s.users.GetAllUsers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return allUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) GetById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
|
||||||
|
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.users.GetUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) Create(ctx context.Context, user *domain.User) (*domain.User, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createdUser, err := s.users.CreateUser(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) Update(ctx context.Context, id domain.UserIdentifier, user *domain.User) (
|
||||||
|
*domain.User,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != user.Identifier {
|
||||||
|
return nil, fmt.Errorf("user id mismatch: %s != %s: %w", id, user.Identifier, domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedUser, err := s.users.UpdateUser(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s UserService) Delete(ctx context.Context, id domain.UserIdentifier) error {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.users.DeleteUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
81
internal/app/api/v1/handlers/base.go
Normal file
81
internal/app/api/v1/handlers/base.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/core"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
GetName() string
|
||||||
|
RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To compile the API documentation use the
|
||||||
|
// api_build_tool
|
||||||
|
// command that can be found in the $PROJECT_ROOT/cmd/api_build_tool directory.
|
||||||
|
|
||||||
|
// @title WireGuard Portal Public API
|
||||||
|
// @version 1.0
|
||||||
|
// @description The WireGuard Portal REST API enables efficient management of WireGuard VPN configurations through a set of JSON-based endpoints.
|
||||||
|
// @description It supports creating and editing peers, interfaces, and user profiles, while also providing role-based access control and auditing.
|
||||||
|
// @description This API allows seamless integration with external tools or scripts for automated network configuration and administration.
|
||||||
|
|
||||||
|
// @license.name MIT
|
||||||
|
// @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt
|
||||||
|
|
||||||
|
// @contact.name WireGuard Portal Project
|
||||||
|
// @contact.url https://github.com/h44z/wg-portal
|
||||||
|
|
||||||
|
// @securityDefinitions.basic BasicAuth
|
||||||
|
|
||||||
|
// @BasePath /api/v1
|
||||||
|
// @query.collection.format multi
|
||||||
|
|
||||||
|
func NewRestApi(userSource UserSource, handlers ...Handler) core.ApiEndpointSetupFunc {
|
||||||
|
authenticator := &authenticationHandler{
|
||||||
|
userSource: userSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() (core.ApiVersion, core.GroupSetupFn) {
|
||||||
|
return "v1", func(group *gin.RouterGroup) {
|
||||||
|
group.Use(cors.Default())
|
||||||
|
|
||||||
|
// Handler functions
|
||||||
|
for _, h := range handlers {
|
||||||
|
h.RegisterRoutes(group, authenticator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseServiceError(err error) (int, models.Error) {
|
||||||
|
if err == nil {
|
||||||
|
return 500, models.Error{
|
||||||
|
Code: 500,
|
||||||
|
Message: "unknown server error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code := http.StatusInternalServerError
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, domain.ErrNotFound):
|
||||||
|
code = http.StatusNotFound
|
||||||
|
case errors.Is(err, domain.ErrNoPermission):
|
||||||
|
code = http.StatusForbidden
|
||||||
|
case errors.Is(err, domain.ErrDuplicateEntry):
|
||||||
|
code = http.StatusConflict
|
||||||
|
case errors.Is(err, domain.ErrInvalidData):
|
||||||
|
code = http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, models.Error{
|
||||||
|
Code: code,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
220
internal/app/api/v1/handlers/endpoint_interface.go
Normal file
220
internal/app/api/v1/handlers/endpoint_interface.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InterfaceEndpointInterfaceService interface {
|
||||||
|
GetAll(context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||||
|
GetById(context.Context, domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
Create(context.Context, *domain.Interface) (*domain.Interface, error)
|
||||||
|
Update(context.Context, domain.InterfaceIdentifier, *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||||
|
Delete(context.Context, domain.InterfaceIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceEndpoint struct {
|
||||||
|
interfaces InterfaceEndpointInterfaceService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterfaceEndpoint(interfaceService InterfaceEndpointInterfaceService) *InterfaceEndpoint {
|
||||||
|
return &InterfaceEndpoint{
|
||||||
|
interfaces: interfaceService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e InterfaceEndpoint) GetName() string {
|
||||||
|
return "InterfaceEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e InterfaceEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/interface", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
||||||
|
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleByIdGet())
|
||||||
|
|
||||||
|
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
|
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||||
|
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID interface_handleAllGet
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Get all interface records.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.Interface
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/all [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleAllGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
allInterfaces, allPeersPerInterface, err := e.interfaces.GetAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterfaces(allInterfaces, allPeersPerInterface))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleByIdGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleByIdGet
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Get a specific interface record by its identifier.
|
||||||
|
// @Param id path string true "The interface identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Interface
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/by-id/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iface, interfacePeers, err := e.interfaces.GetById(ctx, domain.InterfaceIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterface(iface, interfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreatePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleCreatePost
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Create a new interface record.
|
||||||
|
// @Param request body models.Interface true "The interface data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Interface
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 409 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/new [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var iface models.Interface
|
||||||
|
err := c.BindJSON(&iface)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newInterface, err := e.interfaces.Create(ctx, models.NewDomainInterface(&iface))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterface(newInterface, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdatePut returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleUpdatePut
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Update an interface record.
|
||||||
|
// @Param id path string true "The interface identifier."
|
||||||
|
// @Param request body models.Interface true "The interface data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Interface
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/by-id/{id} [put]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var iface models.Interface
|
||||||
|
err := c.BindJSON(&iface)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedInterface, updatedInterfacePeers, err := e.interfaces.Update(
|
||||||
|
ctx,
|
||||||
|
domain.InterfaceIdentifier(id),
|
||||||
|
models.NewDomainInterface(&iface),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewInterface(updatedInterface, updatedInterfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelete returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleDelete
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Delete the interface record.
|
||||||
|
// @Param id path string true "The interface identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204 "No content if deletion was successful."
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/by-id/{id} [delete]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handleDelete() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.interfaces.Delete(ctx, domain.InterfaceIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
261
internal/app/api/v1/handlers/endpoint_peer.go
Normal file
261
internal/app/api/v1/handlers/endpoint_peer.go
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PeerService interface {
|
||||||
|
GetForInterface(context.Context, domain.InterfaceIdentifier) ([]domain.Peer, error)
|
||||||
|
GetForUser(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
|
GetById(context.Context, domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
Create(context.Context, *domain.Peer) (*domain.Peer, error)
|
||||||
|
Update(context.Context, domain.PeerIdentifier, *domain.Peer) (*domain.Peer, error)
|
||||||
|
Delete(context.Context, domain.PeerIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerEndpoint struct {
|
||||||
|
peers PeerService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeerEndpoint(peerService PeerService) *PeerEndpoint {
|
||||||
|
return &PeerEndpoint{
|
||||||
|
peers: peerService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PeerEndpoint) GetName() string {
|
||||||
|
return "PeerEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PeerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/peer", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/by-interface/:id", authenticator.LoggedIn(ScopeAdmin), e.handleAllForInterfaceGet())
|
||||||
|
apiGroup.GET("/by-user/:id", authenticator.LoggedIn(), e.handleAllForUserGet())
|
||||||
|
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
|
||||||
|
|
||||||
|
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
|
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||||
|
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllForInterfaceGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleAllForInterfaceGet
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Get all peer records for a given WireGuard interface.
|
||||||
|
// @Param id path string true "The WireGuard interface identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.Peer
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-interface/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleAllForInterfaceGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interfacePeers, err := e.peers.GetForInterface(ctx, domain.InterfaceIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllForUserGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleAllForUserGet
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Get all peer records for a given user.
|
||||||
|
// @Description Normal users can only access their own records. Admins can access all records.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.Peer
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-user/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleAllForUserGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interfacePeers, err := e.peers.GetForUser(ctx, domain.UserIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleByIdGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleByIdGet
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Get a specific peer record by its identifier (public key).
|
||||||
|
// @Description Normal users can only access their own records. Admins can access all records.
|
||||||
|
// @Param id path string true "The peer identifier (public key)."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-id/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := e.peers.GetById(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreatePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleCreatePost
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Create a new peer record.
|
||||||
|
// @Description Only admins can create new records.
|
||||||
|
// @Param request body models.Peer true "The peer data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 409 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/new [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var peer models.Peer
|
||||||
|
err := c.BindJSON(&peer)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPeer, err := e.peers.Create(ctx, models.NewDomainPeer(&peer))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(newPeer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdatePut returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleUpdatePut
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Update a peer record.
|
||||||
|
// @Description Only admins can update existing records.
|
||||||
|
// @Param id path string true "The peer identifier."
|
||||||
|
// @Param request body models.Peer true "The peer data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-id/{id} [put]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var peer models.Peer
|
||||||
|
err := c.BindJSON(&peer)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedPeer, err := e.peers.Update(ctx, domain.PeerIdentifier(id), models.NewDomainPeer(&peer))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(updatedPeer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelete returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleDelete
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Delete the peer record.
|
||||||
|
// @Param id path string true "The peer identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204 "No content if deletion was successful."
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /peer/by-id/{id} [delete]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handleDelete() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.peers.Delete(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
195
internal/app/api/v1/handlers/endpoint_provisioning.go
Normal file
195
internal/app/api/v1/handlers/endpoint_provisioning.go
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProvisioningEndpointProvisioningService interface {
|
||||||
|
GetUserAndPeers(ctx context.Context, userId domain.UserIdentifier, email string) (
|
||||||
|
*domain.User,
|
||||||
|
[]domain.Peer,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
GetPeerConfig(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
|
||||||
|
GetPeerQrPng(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
|
||||||
|
NewPeer(ctx context.Context, req models.ProvisioningRequest) (*domain.Peer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProvisioningEndpoint struct {
|
||||||
|
provisioning ProvisioningEndpointProvisioningService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProvisioningEndpoint(provisioning ProvisioningEndpointProvisioningService) *ProvisioningEndpoint {
|
||||||
|
return &ProvisioningEndpoint{
|
||||||
|
provisioning: provisioning,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ProvisioningEndpoint) GetName() string {
|
||||||
|
return "ProvisioningEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ProvisioningEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/provisioning", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/data/user-info", authenticator.LoggedIn(), e.handleUserInfoGet())
|
||||||
|
apiGroup.GET("/data/peer-config", authenticator.LoggedIn(), e.handlePeerConfigGet())
|
||||||
|
apiGroup.GET("/data/peer-qr", authenticator.LoggedIn(), e.handlePeerQrGet())
|
||||||
|
|
||||||
|
apiGroup.POST("/new-peer", authenticator.LoggedIn(), e.handleNewPeerPost())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUserInfoGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handleUserInfoGet
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Get information about all peer records for a given user.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param UserId query string false "The user identifier that should be queried. If not set, the authenticated user is used."
|
||||||
|
// @Param Email query string false "The email address that should be queried. If UserId is set, this is ignored."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.UserInformation
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/data/user-info [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handleUserInfoGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := strings.TrimSpace(c.Query("UserId"))
|
||||||
|
email := strings.TrimSpace(c.Query("Email"))
|
||||||
|
|
||||||
|
if id == "" && email == "" {
|
||||||
|
id = string(domain.GetUserInfo(ctx).Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, peers, err := e.provisioning.GetUserAndPeers(ctx, domain.UserIdentifier(id), email)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUserInformation(user, peers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePeerConfigGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handlePeerConfigGet
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Get the peer configuration in wg-quick format.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
|
||||||
|
// @Produce plain
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {string} string "The WireGuard configuration file"
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/data/peer-config [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handlePeerConfigGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := strings.TrimSpace(c.Query("PeerId"))
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConfig, err := e.provisioning.GetPeerConfig(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, "text/plain", peerConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePeerQrGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handlePeerQrGet
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Get the peer configuration as QR code.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
|
||||||
|
// @Produce png
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {file} binary "The WireGuard configuration QR code"
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/data/peer-qr [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handlePeerQrGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := strings.TrimSpace(c.Query("PeerId"))
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConfigQrCode, err := e.provisioning.GetPeerQrPng(ctx, domain.PeerIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, "image/png", peerConfigQrCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNewPeerPost returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID provisioning_handleNewPeerPost
|
||||||
|
// @Tags Provisioning
|
||||||
|
// @Summary Create a new peer for the given interface and user.
|
||||||
|
// @Description Normal users can only create new peers if self provisioning is allowed. Admins can always add new peers.
|
||||||
|
// @Param request body models.ProvisioningRequest true "Provisioning request model."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Peer
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /provisioning/new-peer [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e ProvisioningEndpoint) handleNewPeerPost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var req models.ProvisioningRequest
|
||||||
|
err := c.BindJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := e.provisioning.NewPeer(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewPeer(peer))
|
||||||
|
}
|
||||||
|
}
|
218
internal/app/api/v1/handlers/endpoint_user.go
Normal file
218
internal/app/api/v1/handlers/endpoint_user.go
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserService interface {
|
||||||
|
GetAll(ctx context.Context) ([]domain.User, error)
|
||||||
|
GetById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
Create(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
|
Update(ctx context.Context, id domain.UserIdentifier, user *domain.User) (*domain.User, error)
|
||||||
|
Delete(ctx context.Context, id domain.UserIdentifier) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserEndpoint struct {
|
||||||
|
users UserService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserEndpoint(userService UserService) *UserEndpoint {
|
||||||
|
return &UserEndpoint{
|
||||||
|
users: userService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UserEndpoint) GetName() string {
|
||||||
|
return "UserEndpoint"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e UserEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||||
|
apiGroup := g.Group("/user", authenticator.LoggedIn())
|
||||||
|
|
||||||
|
apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
||||||
|
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
|
||||||
|
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
|
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||||
|
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAllGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleAllGet
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Get all user records.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []models.User
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/all [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleAllGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
users, err := e.users.GetAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUsers(users))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleByIdGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleByIdGet
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Get a specific user record by its internal identifier.
|
||||||
|
// @Description Normal users can only access their own record. Admins can access all records.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/by-id/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := e.users.GetById(ctx, domain.UserIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUser(user, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCreatePost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleCreatePost
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Create a new user record.
|
||||||
|
// @Description Only admins can create new records.
|
||||||
|
// @Param request body models.User true "The user data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 409 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/new [post]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err := c.BindJSON(&user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newUser, err := e.users.Create(ctx, models.NewDomainUser(&user))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUser(newUser, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdatePut returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleUpdatePut
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Update a user record.
|
||||||
|
// @Description Only admins can update existing records.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Param request body models.User true "The user data."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/by-id/{id} [put]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err := c.BindJSON(&user)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser, err := e.users.Update(ctx, domain.UserIdentifier(id), models.NewDomainUser(&user))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, models.NewUser(updateUser, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelete returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleDelete
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Delete the user record.
|
||||||
|
// @Param id path string true "The user identifier."
|
||||||
|
// @Produce json
|
||||||
|
// @Success 204 "No content if deletion was successful."
|
||||||
|
// @Failure 400 {object} models.Error
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 404 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /user/by-id/{id} [delete]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e UserEndpoint) handleDelete() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.users.Delete(ctx, domain.UserIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(ParseServiceError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
92
internal/app/api/v1/handlers/middleware_authentication.go
Normal file
92
internal/app/api/v1/handlers/middleware_authentication.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scope string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScopeAdmin Scope = "ADMIN" // Admin scope contains all other scopes
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserSource interface {
|
||||||
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type authenticationHandler struct {
|
||||||
|
userSource UserSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggedIn checks if a user is logged in. If scopes are given, they are validated as well.
|
||||||
|
func (h authenticationHandler) LoggedIn(scopes ...Scope) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
username, password, ok := c.Request.BasicAuth()
|
||||||
|
if !ok || username == "" || password == "" {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "missing credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user exists in DB
|
||||||
|
|
||||||
|
ctx := domain.SetUserInfo(c.Request.Context(), domain.SystemAdminContextUserInfo())
|
||||||
|
user, err := h.userSource.GetUser(ctx, domain.UserIdentifier(username))
|
||||||
|
if err != nil {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate API token
|
||||||
|
if err := user.CheckApiToken(password); err != nil {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !UserHasScopes(user, scopes...) {
|
||||||
|
// Abort the request with the appropriate error code
|
||||||
|
c.Abort()
|
||||||
|
c.JSON(http.StatusForbidden, model.Error{Code: http.StatusForbidden, Message: "not enough permissions"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set(domain.CtxUserInfo, &domain.ContextUserInfo{
|
||||||
|
Id: user.Identifier,
|
||||||
|
IsAdmin: user.IsAdmin,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Continue down the chain to Handler etc
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserHasScopes(user *domain.User, scopes ...Scope) bool {
|
||||||
|
// No scopes give, so the check should succeed
|
||||||
|
if len(scopes) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if user has admin scope
|
||||||
|
if user.IsAdmin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if admin scope is required
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if scope == ScopeAdmin {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
46
internal/app/api/v1/models/model_options.go
Normal file
46
internal/app/api/v1/models/model_options.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigOption[T any] struct {
|
||||||
|
Value T `json:"Value"`
|
||||||
|
Overridable bool `json:"Overridable,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigOption[T any](value T, overridable bool) ConfigOption[T] {
|
||||||
|
return ConfigOption[T]{
|
||||||
|
Value: value,
|
||||||
|
Overridable: overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigOptionFromDomain[T any](opt domain.ConfigOption[T]) ConfigOption[T] {
|
||||||
|
return ConfigOption[T]{
|
||||||
|
Value: opt.Value,
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigOptionToDomain[T any](opt ConfigOption[T]) domain.ConfigOption[T] {
|
||||||
|
return domain.ConfigOption[T]{
|
||||||
|
Value: opt.Value,
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSliceConfigOptionFromDomain(opt domain.ConfigOption[string]) ConfigOption[[]string] {
|
||||||
|
return ConfigOption[[]string]{
|
||||||
|
Value: internal.SliceString(opt.Value),
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringSliceConfigOptionToDomain(opt ConfigOption[[]string]) domain.ConfigOption[string] {
|
||||||
|
return domain.ConfigOption[string]{
|
||||||
|
Value: internal.SliceToString(opt.Value),
|
||||||
|
Overridable: opt.Overridable,
|
||||||
|
}
|
||||||
|
}
|
8
internal/app/api/v1/models/models.go
Normal file
8
internal/app/api/v1/models/models.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
// Error represents an error response.
|
||||||
|
type Error struct {
|
||||||
|
Code int `json:"Code"` // HTTP status code.
|
||||||
|
Message string `json:"Message"` // Error message.
|
||||||
|
Details string `json:"Details,omitempty"` // Additional error details.
|
||||||
|
}
|
201
internal/app/api/v1/models/models_interface.go
Normal file
201
internal/app/api/v1/models/models_interface.go
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface represents a WireGuard interface.
|
||||||
|
type Interface struct {
|
||||||
|
// Identifier is the unique identifier of the interface. It is always equal to the device name of the interface.
|
||||||
|
Identifier string `json:"Identifier" example:"wg0" binding:"required"`
|
||||||
|
// DisplayName is a nice display name / description for the interface.
|
||||||
|
DisplayName string `json:"DisplayName" binding:"omitempty,max=64" example:"My Interface"`
|
||||||
|
// Mode is the interface type, either 'server', 'client' or 'any'. The mode specifies how WireGuard Portal handles peers for this interface.
|
||||||
|
Mode string `json:"Mode" example:"server" binding:"required,oneof=server client any"`
|
||||||
|
// PrivateKey is the private key of the interface.
|
||||||
|
PrivateKey string `json:"PrivateKey" example:"gI6EdUSYvn8ugXOt8QQD6Yc+JyiZxIhp3GInSWRfWGE=" binding:"required,len=44"`
|
||||||
|
// PublicKey is the public key of the server interface. The public key is used by peers to connect to the server.
|
||||||
|
PublicKey string `json:"PublicKey" example:"HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=" binding:"required,len=44"`
|
||||||
|
// Disabled is a flag that specifies if the interface is enabled (up) or not (down). Disabled interfaces are not able to accept connections.
|
||||||
|
Disabled bool `json:"Disabled" example:"false"`
|
||||||
|
// DisabledReason is the reason why the interface has been disabled.
|
||||||
|
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:"This is a reason why the interface has been disabled."`
|
||||||
|
// SaveConfig is a flag that specifies if the configuration should be saved to the configuration file (wgX.conf in wg-quick format).
|
||||||
|
SaveConfig bool `json:"SaveConfig" example:"false"`
|
||||||
|
|
||||||
|
// ListenPort is the listening port, for example: 51820. The listening port is only required for server interfaces.
|
||||||
|
ListenPort int `json:"ListenPort" binding:"omitempty,min=1,max=65535" example:"51820"`
|
||||||
|
// Addresses is a list of IP addresses (in CIDR format) that are assigned to the interface.
|
||||||
|
Addresses []string `json:"Addresses" binding:"omitempty,dive,cidr" example:"10.11.12.1/24"`
|
||||||
|
// Dns is a list of DNS servers that should be set if the interface is up.
|
||||||
|
Dns []string `json:"Dns" binding:"omitempty,dive,ip" example:"1.1.1.1"`
|
||||||
|
// DnsSearch is the dns search option string that should be set if the interface is up, will be appended to Dns servers.
|
||||||
|
DnsSearch []string `json:"DnsSearch" binding:"omitempty,dive,fqdn" example:"wg.local"`
|
||||||
|
// Mtu is the device MTU of the interface.
|
||||||
|
Mtu int `json:"Mtu" binding:"omitempty,min=1,max=9000" example:"1420"`
|
||||||
|
// FirewallMark is an optional firewall mark which is used to handle interface traffic.
|
||||||
|
FirewallMark uint32 `json:"FirewallMark"`
|
||||||
|
// RoutingTable is an optional routing table which is used to route interface traffic.
|
||||||
|
RoutingTable string `json:"RoutingTable"`
|
||||||
|
|
||||||
|
// PreUp is an optional action that is executed before the device is up.
|
||||||
|
PreUp string `json:"PreUp" example:"echo 'Interface is up'"`
|
||||||
|
// PostUp is an optional action that is executed after the device is up.
|
||||||
|
PostUp string `json:"PostUp" example:"iptables -A FORWARD -i %i -j ACCEPT"`
|
||||||
|
// PreDown is an optional action that is executed before the device is down.
|
||||||
|
PreDown string `json:"PreDown" example:"iptables -D FORWARD -i %i -j ACCEPT"`
|
||||||
|
// PostDown is an optional action that is executed after the device is down.
|
||||||
|
PostDown string `json:"PostDown" example:"echo 'Interface is down'"`
|
||||||
|
|
||||||
|
// PeerDefNetwork specifies the default subnets from which new peers will get their IP addresses. The subnet is specified in CIDR format.
|
||||||
|
PeerDefNetwork []string `json:"PeerDefNetwork" example:"10.11.12.0/24"`
|
||||||
|
// PeerDefDns specifies the default dns servers for a new peer.
|
||||||
|
PeerDefDns []string `json:"PeerDefDns" example:"8.8.8.8"`
|
||||||
|
// PeerDefDnsSearch specifies the default dns search options for a new peer.
|
||||||
|
PeerDefDnsSearch []string `json:"PeerDefDnsSearch" example:"wg.local"`
|
||||||
|
// PeerDefEndpoint specifies the default endpoint for a new peer.
|
||||||
|
PeerDefEndpoint string `json:"PeerDefEndpoint" example:"wg.example.com:51820"`
|
||||||
|
// PeerDefAllowedIPs specifies the default allowed IP addresses for a new peer.
|
||||||
|
PeerDefAllowedIPs []string `json:"PeerDefAllowedIPs" example:"10.11.12.0/24"`
|
||||||
|
// PeerDefMtu specifies the default device MTU for a new peer.
|
||||||
|
PeerDefMtu int `json:"PeerDefMtu" example:"1420"`
|
||||||
|
// PeerDefPersistentKeepalive specifies the default persistent keep-alive value in seconds for a new peer.
|
||||||
|
PeerDefPersistentKeepalive int `json:"PeerDefPersistentKeepalive" example:"25"`
|
||||||
|
// PeerDefFirewallMark specifies the default firewall mark for a new peer.
|
||||||
|
PeerDefFirewallMark uint32 `json:"PeerDefFirewallMark"`
|
||||||
|
// PeerDefRoutingTable specifies the default routing table for a new peer.
|
||||||
|
PeerDefRoutingTable string `json:"PeerDefRoutingTable"`
|
||||||
|
|
||||||
|
// PeerDefPreUp specifies the default action that is executed before the device is up for a new peer.
|
||||||
|
PeerDefPreUp string `json:"PeerDefPreUp"`
|
||||||
|
// PeerDefPostUp specifies the default action that is executed after the device is up for a new peer.
|
||||||
|
PeerDefPostUp string `json:"PeerDefPostUp"`
|
||||||
|
// PeerDefPreDown specifies the default action that is executed before the device is down for a new peer.
|
||||||
|
PeerDefPreDown string `json:"PeerDefPreDown"`
|
||||||
|
// PeerDefPostDown specifies the default action that is executed after the device is down for a new peer.
|
||||||
|
PeerDefPostDown string `json:"PeerDefPostDown"`
|
||||||
|
|
||||||
|
// Calculated values
|
||||||
|
|
||||||
|
// EnabledPeers is the number of enabled peers for this interface. Only enabled peers are able to connect.
|
||||||
|
EnabledPeers int `json:"EnabledPeers" readonly:"true"`
|
||||||
|
// TotalPeers is the total number of peers for this interface.
|
||||||
|
TotalPeers int `json:"TotalPeers" readonly:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
|
||||||
|
iface := &Interface{
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
Mode: string(src.Type),
|
||||||
|
PrivateKey: src.PrivateKey,
|
||||||
|
PublicKey: src.PublicKey,
|
||||||
|
Disabled: src.IsDisabled(),
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
SaveConfig: src.SaveConfig,
|
||||||
|
ListenPort: src.ListenPort,
|
||||||
|
Addresses: domain.CidrsToStringSlice(src.Addresses),
|
||||||
|
Dns: internal.SliceString(src.DnsStr),
|
||||||
|
DnsSearch: internal.SliceString(src.DnsSearchStr),
|
||||||
|
Mtu: src.Mtu,
|
||||||
|
FirewallMark: src.FirewallMark,
|
||||||
|
RoutingTable: src.RoutingTable,
|
||||||
|
PreUp: src.PreUp,
|
||||||
|
PostUp: src.PostUp,
|
||||||
|
PreDown: src.PreDown,
|
||||||
|
PostDown: src.PostDown,
|
||||||
|
PeerDefNetwork: internal.SliceString(src.PeerDefNetworkStr),
|
||||||
|
PeerDefDns: internal.SliceString(src.PeerDefDnsStr),
|
||||||
|
PeerDefDnsSearch: internal.SliceString(src.PeerDefDnsSearchStr),
|
||||||
|
PeerDefEndpoint: src.PeerDefEndpoint,
|
||||||
|
PeerDefAllowedIPs: internal.SliceString(src.PeerDefAllowedIPsStr),
|
||||||
|
PeerDefMtu: src.PeerDefMtu,
|
||||||
|
PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive,
|
||||||
|
PeerDefFirewallMark: src.PeerDefFirewallMark,
|
||||||
|
PeerDefRoutingTable: src.PeerDefRoutingTable,
|
||||||
|
PeerDefPreUp: src.PeerDefPreUp,
|
||||||
|
PeerDefPostUp: src.PeerDefPostUp,
|
||||||
|
PeerDefPreDown: src.PeerDefPreDown,
|
||||||
|
PeerDefPostDown: src.PeerDefPostDown,
|
||||||
|
|
||||||
|
EnabledPeers: 0,
|
||||||
|
TotalPeers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(peers) > 0 {
|
||||||
|
iface.TotalPeers = len(peers)
|
||||||
|
|
||||||
|
activePeers := 0
|
||||||
|
for _, peer := range peers {
|
||||||
|
if !peer.IsDisabled() {
|
||||||
|
activePeers++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iface.EnabledPeers = activePeers
|
||||||
|
}
|
||||||
|
|
||||||
|
return iface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface {
|
||||||
|
results := make([]Interface, len(src))
|
||||||
|
for i := range src {
|
||||||
|
results[i] = *NewInterface(&src[i], srcPeers[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainInterface(src *Interface) *domain.Interface {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
cidrs, _ := domain.CidrsFromArray(src.Addresses)
|
||||||
|
|
||||||
|
res := &domain.Interface{
|
||||||
|
BaseModel: domain.BaseModel{},
|
||||||
|
Identifier: domain.InterfaceIdentifier(src.Identifier),
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PrivateKey: src.PrivateKey,
|
||||||
|
PublicKey: src.PublicKey,
|
||||||
|
},
|
||||||
|
ListenPort: src.ListenPort,
|
||||||
|
Addresses: cidrs,
|
||||||
|
DnsStr: internal.SliceToString(src.Dns),
|
||||||
|
DnsSearchStr: internal.SliceToString(src.DnsSearch),
|
||||||
|
Mtu: src.Mtu,
|
||||||
|
FirewallMark: src.FirewallMark,
|
||||||
|
RoutingTable: src.RoutingTable,
|
||||||
|
PreUp: src.PreUp,
|
||||||
|
PostUp: src.PostUp,
|
||||||
|
PreDown: src.PreDown,
|
||||||
|
PostDown: src.PostDown,
|
||||||
|
SaveConfig: src.SaveConfig,
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
Type: domain.InterfaceType(src.Mode),
|
||||||
|
DriverType: "", // currently unused
|
||||||
|
Disabled: nil, // set below
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
PeerDefNetworkStr: internal.SliceToString(src.PeerDefNetwork),
|
||||||
|
PeerDefDnsStr: internal.SliceToString(src.PeerDefDns),
|
||||||
|
PeerDefDnsSearchStr: internal.SliceToString(src.PeerDefDnsSearch),
|
||||||
|
PeerDefEndpoint: src.PeerDefEndpoint,
|
||||||
|
PeerDefAllowedIPsStr: internal.SliceToString(src.PeerDefAllowedIPs),
|
||||||
|
PeerDefMtu: src.PeerDefMtu,
|
||||||
|
PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive,
|
||||||
|
PeerDefFirewallMark: src.PeerDefFirewallMark,
|
||||||
|
PeerDefRoutingTable: src.PeerDefRoutingTable,
|
||||||
|
PeerDefPreUp: src.PeerDefPreUp,
|
||||||
|
PeerDefPostUp: src.PeerDefPostUp,
|
||||||
|
PeerDefPreDown: src.PeerDefPreDown,
|
||||||
|
PeerDefPostDown: src.PeerDefPostDown,
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Disabled {
|
||||||
|
res.Disabled = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
195
internal/app/api/v1/models/models_peer.go
Normal file
195
internal/app/api/v1/models/models_peer.go
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ExpiryDateTimeLayout = "\"2006-01-02\""
|
||||||
|
|
||||||
|
type ExpiryDate struct {
|
||||||
|
*time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON will unmarshal using 2006-01-02 layout
|
||||||
|
func (d *ExpiryDate) UnmarshalJSON(b []byte) error {
|
||||||
|
if len(b) == 0 || string(b) == "null" || string(b) == "\"\"" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(ExpiryDateTimeLayout, string(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parsed.IsZero() {
|
||||||
|
d.Time = &parsed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON will marshal using 2006-01-02 layout
|
||||||
|
func (d *ExpiryDate) MarshalJSON() ([]byte, error) {
|
||||||
|
if d == nil || d.Time == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s := d.Format(ExpiryDateTimeLayout)
|
||||||
|
return []byte(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peer represents a WireGuard peer entry.
|
||||||
|
type Peer struct {
|
||||||
|
// Identifier is the unique identifier of the peer. It is always equal to the public key of the peer.
|
||||||
|
Identifier string `json:"Identifier" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"required,len=44"`
|
||||||
|
// DisplayName is a nice display name / description for the peer.
|
||||||
|
DisplayName string `json:"DisplayName" example:"My Peer" binding:"omitempty,max=64"`
|
||||||
|
// UserIdentifier is the identifier of the user that owns the peer.
|
||||||
|
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||||
|
// InterfaceIdentifier is the identifier of the interface the peer is linked to.
|
||||||
|
InterfaceIdentifier string `json:"InterfaceIdentifier" binding:"required" example:"wg0"`
|
||||||
|
// Disabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
|
||||||
|
Disabled bool `json:"Disabled" example:"false"`
|
||||||
|
// DisabledReason is the reason why the peer has been disabled.
|
||||||
|
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:"This is a reason why the peer has been disabled."`
|
||||||
|
// ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. An expired peer is not able to connect.
|
||||||
|
ExpiresAt ExpiryDate `json:"ExpiresAt,omitempty" binding:"omitempty,datetime=2006-01-02"`
|
||||||
|
// Notes is a note field for peers.
|
||||||
|
Notes string `json:"Notes" example:"This is a note for the peer."`
|
||||||
|
|
||||||
|
// Endpoint is the endpoint address of the peer.
|
||||||
|
Endpoint ConfigOption[string] `json:"Endpoint"`
|
||||||
|
// EndpointPublicKey is the endpoint public key.
|
||||||
|
EndpointPublicKey ConfigOption[string] `json:"EndpointPublicKey"`
|
||||||
|
// AllowedIPs is a list of allowed IP subnets for the peer.
|
||||||
|
AllowedIPs ConfigOption[[]string] `json:"AllowedIPs"`
|
||||||
|
// ExtraAllowedIPs is a list of additional allowed IP subnets for the peer. These allowed IP subnets are added on the server side.
|
||||||
|
ExtraAllowedIPs []string `json:"ExtraAllowedIPs"`
|
||||||
|
// PresharedKey is the optional pre-shared Key of the peer.
|
||||||
|
PresharedKey string `json:"PresharedKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"omitempty,len=44"`
|
||||||
|
// PersistentKeepalive is the optional persistent keep-alive interval in seconds.
|
||||||
|
PersistentKeepalive ConfigOption[int] `json:"PersistentKeepalive" binding:"omitempty,gte=0"`
|
||||||
|
|
||||||
|
// PrivateKey is the private Key of the peer.
|
||||||
|
PrivateKey string `json:"PrivateKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"required,len=44"`
|
||||||
|
// PublicKey is the public Key of the server peer.
|
||||||
|
PublicKey string `json:"PublicKey" example:"TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=" binding:"omitempty,len=44"`
|
||||||
|
|
||||||
|
// Mode is the peer interface type (server, client, any).
|
||||||
|
Mode string `json:"Mode" example:"client" binding:"omitempty,oneof=server client any"`
|
||||||
|
|
||||||
|
// Addresses is a list of IP addresses in CIDR format (both IPv4 and IPv6) for the peer.
|
||||||
|
Addresses []string `json:"Addresses" example:"10.11.12.2/24" binding:"omitempty,dive,cidr"`
|
||||||
|
// CheckAliveAddress is an optional ip address or DNS name that is used for ping checks.
|
||||||
|
CheckAliveAddress string `json:"CheckAliveAddress" binding:"omitempty,ip|fqdn" example:"1.1.1.1"`
|
||||||
|
// Dns is a list of DNS servers that should be set if the peer interface is up.
|
||||||
|
Dns ConfigOption[[]string] `json:"Dns"`
|
||||||
|
// DnsSearch is the dns search option string that should be set if the peer interface is up, will be appended to Dns servers.
|
||||||
|
DnsSearch ConfigOption[[]string] `json:"DnsSearch"`
|
||||||
|
// Mtu is the device MTU of the peer.
|
||||||
|
Mtu ConfigOption[int] `json:"Mtu"`
|
||||||
|
// FirewallMark is an optional firewall mark which is used to handle peer traffic.
|
||||||
|
FirewallMark ConfigOption[uint32] `json:"FirewallMark"`
|
||||||
|
// RoutingTable is an optional routing table which is used to route peer traffic.
|
||||||
|
RoutingTable ConfigOption[string] `json:"RoutingTable"`
|
||||||
|
|
||||||
|
// PreUp is an optional action that is executed before the device is up.
|
||||||
|
PreUp ConfigOption[string] `json:"PreUp"`
|
||||||
|
// PostUp is an optional action that is executed after the device is up.
|
||||||
|
PostUp ConfigOption[string] `json:"PostUp"`
|
||||||
|
// PreDown is an optional action that is executed before the device is down.
|
||||||
|
PreDown ConfigOption[string] `json:"PreDown"`
|
||||||
|
// PostDown is an optional action that is executed after the device is down.
|
||||||
|
PostDown ConfigOption[string] `json:"PostDown"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeer(src *domain.Peer) *Peer {
|
||||||
|
return &Peer{
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
UserIdentifier: string(src.UserIdentifier),
|
||||||
|
InterfaceIdentifier: string(src.InterfaceIdentifier),
|
||||||
|
Disabled: src.IsDisabled(),
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
ExpiresAt: ExpiryDate{src.ExpiresAt},
|
||||||
|
Notes: src.Notes,
|
||||||
|
Endpoint: ConfigOptionFromDomain(src.Endpoint),
|
||||||
|
EndpointPublicKey: ConfigOptionFromDomain(src.EndpointPublicKey),
|
||||||
|
AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr),
|
||||||
|
ExtraAllowedIPs: internal.SliceString(src.ExtraAllowedIPsStr),
|
||||||
|
PresharedKey: string(src.PresharedKey),
|
||||||
|
PersistentKeepalive: ConfigOptionFromDomain(src.PersistentKeepalive),
|
||||||
|
PrivateKey: src.Interface.PrivateKey,
|
||||||
|
PublicKey: src.Interface.PublicKey,
|
||||||
|
Mode: string(src.Interface.Type),
|
||||||
|
Addresses: domain.CidrsToStringSlice(src.Interface.Addresses),
|
||||||
|
CheckAliveAddress: src.Interface.CheckAliveAddress,
|
||||||
|
Dns: StringSliceConfigOptionFromDomain(src.Interface.DnsStr),
|
||||||
|
DnsSearch: StringSliceConfigOptionFromDomain(src.Interface.DnsSearchStr),
|
||||||
|
Mtu: ConfigOptionFromDomain(src.Interface.Mtu),
|
||||||
|
FirewallMark: ConfigOptionFromDomain(src.Interface.FirewallMark),
|
||||||
|
RoutingTable: ConfigOptionFromDomain(src.Interface.RoutingTable),
|
||||||
|
PreUp: ConfigOptionFromDomain(src.Interface.PreUp),
|
||||||
|
PostUp: ConfigOptionFromDomain(src.Interface.PostUp),
|
||||||
|
PreDown: ConfigOptionFromDomain(src.Interface.PreDown),
|
||||||
|
PostDown: ConfigOptionFromDomain(src.Interface.PostDown),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeers(src []domain.Peer) []Peer {
|
||||||
|
results := make([]Peer, len(src))
|
||||||
|
for i := range src {
|
||||||
|
results[i] = *NewPeer(&src[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainPeer(src *Peer) *domain.Peer {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
cidrs, _ := domain.CidrsFromArray(src.Addresses)
|
||||||
|
|
||||||
|
res := &domain.Peer{
|
||||||
|
BaseModel: domain.BaseModel{},
|
||||||
|
Endpoint: ConfigOptionToDomain(src.Endpoint),
|
||||||
|
EndpointPublicKey: ConfigOptionToDomain(src.EndpointPublicKey),
|
||||||
|
AllowedIPsStr: StringSliceConfigOptionToDomain(src.AllowedIPs),
|
||||||
|
ExtraAllowedIPsStr: internal.SliceToString(src.ExtraAllowedIPs),
|
||||||
|
PresharedKey: domain.PreSharedKey(src.PresharedKey),
|
||||||
|
PersistentKeepalive: ConfigOptionToDomain(src.PersistentKeepalive),
|
||||||
|
DisplayName: src.DisplayName,
|
||||||
|
Identifier: domain.PeerIdentifier(src.Identifier),
|
||||||
|
UserIdentifier: domain.UserIdentifier(src.UserIdentifier),
|
||||||
|
InterfaceIdentifier: domain.InterfaceIdentifier(src.InterfaceIdentifier),
|
||||||
|
Disabled: nil, // set below
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
ExpiresAt: src.ExpiresAt.Time,
|
||||||
|
Notes: src.Notes,
|
||||||
|
Interface: domain.PeerInterfaceConfig{
|
||||||
|
KeyPair: domain.KeyPair{
|
||||||
|
PrivateKey: src.PrivateKey,
|
||||||
|
PublicKey: src.PublicKey,
|
||||||
|
},
|
||||||
|
Type: domain.InterfaceType(src.Mode),
|
||||||
|
Addresses: cidrs,
|
||||||
|
CheckAliveAddress: src.CheckAliveAddress,
|
||||||
|
DnsStr: StringSliceConfigOptionToDomain(src.Dns),
|
||||||
|
DnsSearchStr: StringSliceConfigOptionToDomain(src.DnsSearch),
|
||||||
|
Mtu: ConfigOptionToDomain(src.Mtu),
|
||||||
|
FirewallMark: ConfigOptionToDomain(src.FirewallMark),
|
||||||
|
RoutingTable: ConfigOptionToDomain(src.RoutingTable),
|
||||||
|
PreUp: ConfigOptionToDomain(src.PreUp),
|
||||||
|
PostUp: ConfigOptionToDomain(src.PostUp),
|
||||||
|
PreDown: ConfigOptionToDomain(src.PreDown),
|
||||||
|
PostDown: ConfigOptionToDomain(src.PostDown),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Disabled {
|
||||||
|
res.Disabled = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
75
internal/app/api/v1/models/models_provisioning.go
Normal file
75
internal/app/api/v1/models/models_provisioning.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "github.com/h44z/wg-portal/internal/domain"
|
||||||
|
|
||||||
|
// UserInformation represents the information about a user and its linked peers.
|
||||||
|
type UserInformation struct {
|
||||||
|
// UserIdentifier is the unique identifier of the user.
|
||||||
|
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||||
|
// PeerCount is the number of peers linked to the user.
|
||||||
|
PeerCount int `json:"PeerCount" example:"2"`
|
||||||
|
// Peers is a list of peers linked to the user.
|
||||||
|
Peers []UserInformationPeer `json:"Peers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInformationPeer represents the information about a peer.
|
||||||
|
type UserInformationPeer struct {
|
||||||
|
// Identifier is the unique identifier of the peer. It equals the public key of the peer.
|
||||||
|
Identifier string `json:"Identifier" example:"peer-1234567"`
|
||||||
|
// DisplayName is a user-defined description of the peer.
|
||||||
|
DisplayName string `json:"DisplayName" example:"My iPhone"`
|
||||||
|
// IPAddresses is a list of IP addresses in CIDR format assigned to the peer.
|
||||||
|
IpAddresses []string `json:"IpAddresses" example:"10.11.12.2/24"`
|
||||||
|
// IsDisabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
|
||||||
|
IsDisabled bool `json:"IsDisabled,omitempty" example:"true"`
|
||||||
|
|
||||||
|
// InterfaceIdentifier is the unique identifier of the WireGuard Portal device the peer is connected to.
|
||||||
|
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserInformation(user *domain.User, peers []domain.Peer) *UserInformation {
|
||||||
|
if user == nil {
|
||||||
|
return &UserInformation{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui := &UserInformation{
|
||||||
|
UserIdentifier: string(user.Identifier),
|
||||||
|
PeerCount: len(peers),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range peers {
|
||||||
|
ui.Peers = append(ui.Peers, NewUserInformationPeer(peer))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ui.Peers) == 0 {
|
||||||
|
ui.Peers = []UserInformationPeer{} // Ensure that the JSON output is an empty array instead of null.
|
||||||
|
}
|
||||||
|
|
||||||
|
return ui
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserInformationPeer(peer domain.Peer) UserInformationPeer {
|
||||||
|
up := UserInformationPeer{
|
||||||
|
Identifier: string(peer.Identifier),
|
||||||
|
DisplayName: peer.DisplayName,
|
||||||
|
IpAddresses: domain.CidrsToStringSlice(peer.Interface.Addresses),
|
||||||
|
IsDisabled: peer.IsDisabled(),
|
||||||
|
InterfaceIdentifier: string(peer.InterfaceIdentifier),
|
||||||
|
}
|
||||||
|
|
||||||
|
return up
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvisioningRequest represents a request to provision a new peer.
|
||||||
|
type ProvisioningRequest struct {
|
||||||
|
// InterfaceIdentifier is the identifier of the WireGuard interface the peer should be linked to.
|
||||||
|
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0" binding:"required"`
|
||||||
|
// UserIdentifier is the identifier of the user the peer should be linked to.
|
||||||
|
// If no user identifier is set, the authenticated user is used.
|
||||||
|
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
|
||||||
|
|
||||||
|
// PublicKey is the optional public key of the peer. If no public key is set, a new key pair is generated.
|
||||||
|
PublicKey string `json:"PublicKey" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"omitempty,len=44"`
|
||||||
|
// PresharedKey is the optional pre-shared key of the peer. If no pre-shared key is set, a new key is generated.
|
||||||
|
PresharedKey string `json:"PresharedKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"omitempty,len=44"`
|
||||||
|
}
|
125
internal/app/api/v1/models/models_user.go
Normal file
125
internal/app/api/v1/models/models_user.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User represents a user in the system.
|
||||||
|
type User struct {
|
||||||
|
// The unique identifier of the user.
|
||||||
|
Identifier string `json:"Identifier" binding:"required,max=64" example:"uid-1234567"`
|
||||||
|
// The email address of the user. This field is optional.
|
||||||
|
Email string `json:"Email" binding:"omitempty,email" example:"test@test.com"`
|
||||||
|
// The source of the user. This field is optional.
|
||||||
|
Source string `json:"Source" binding:"oneof=db" example:"db"`
|
||||||
|
// The name of the authentication provider. This field is read-only.
|
||||||
|
ProviderName string `json:"ProviderName,omitempty" readonly:"true" example:""`
|
||||||
|
// If this field is set, the user is an admin.
|
||||||
|
IsAdmin bool `json:"IsAdmin" binding:"required" example:"false"`
|
||||||
|
|
||||||
|
// The first name of the user. This field is optional.
|
||||||
|
Firstname string `json:"Firstname" example:"Max"`
|
||||||
|
// The last name of the user. This field is optional.
|
||||||
|
Lastname string `json:"Lastname" example:"Muster"`
|
||||||
|
// The phone number of the user. This field is optional.
|
||||||
|
Phone string `json:"Phone" example:"+1234546789"`
|
||||||
|
// The department of the user. This field is optional.
|
||||||
|
Department string `json:"Department" example:"Software Development"`
|
||||||
|
// Additional notes about the user. This field is optional.
|
||||||
|
Notes string `json:"Notes" example:"some sample notes"`
|
||||||
|
|
||||||
|
// The password of the user. This field is never populated on read operations.
|
||||||
|
Password string `json:"Password,omitempty" binding:"omitempty,min=16,max=64" example:""`
|
||||||
|
// If this field is set, the user is disabled.
|
||||||
|
Disabled bool `json:"Disabled" example:"false"`
|
||||||
|
// The reason why the user has been disabled.
|
||||||
|
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:""`
|
||||||
|
// If this field is set, the user is locked and thus unable to log in to WireGuard Portal.
|
||||||
|
Locked bool `json:"Locked" example:"false"`
|
||||||
|
// The reason why the user has been locked.
|
||||||
|
LockedReason string `json:"LockedReason" binding:"required_if=Locked true" example:""`
|
||||||
|
|
||||||
|
// The API token of the user. This field is never populated on bulk read operations.
|
||||||
|
ApiToken string `json:"ApiToken,omitempty" binding:"omitempty,min=32,max=64" example:""`
|
||||||
|
// If this field is set, the user is allowed to use the RESTful API. This field is read-only.
|
||||||
|
ApiEnabled bool `json:"ApiEnabled" readonly:"true" example:"false"`
|
||||||
|
|
||||||
|
// The number of peers linked to the user. This field is read-only.
|
||||||
|
PeerCount int `json:"PeerCount" readonly:"true" example:"2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUser(src *domain.User, exposeCredentials bool) *User {
|
||||||
|
u := &User{
|
||||||
|
Identifier: string(src.Identifier),
|
||||||
|
Email: src.Email,
|
||||||
|
Source: string(src.Source),
|
||||||
|
ProviderName: src.ProviderName,
|
||||||
|
IsAdmin: src.IsAdmin,
|
||||||
|
Firstname: src.Firstname,
|
||||||
|
Lastname: src.Lastname,
|
||||||
|
Phone: src.Phone,
|
||||||
|
Department: src.Department,
|
||||||
|
Notes: src.Notes,
|
||||||
|
Password: "", // never fill password
|
||||||
|
Disabled: src.IsDisabled(),
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
Locked: src.IsLocked(),
|
||||||
|
LockedReason: src.LockedReason,
|
||||||
|
ApiToken: "", // by default, do not expose API token
|
||||||
|
ApiEnabled: src.IsApiEnabled(),
|
||||||
|
PeerCount: src.LinkedPeerCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
if exposeCredentials {
|
||||||
|
u.ApiToken = src.ApiToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUsers(src []domain.User) []User {
|
||||||
|
results := make([]User, len(src))
|
||||||
|
for i := range src {
|
||||||
|
results[i] = *NewUser(&src[i], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainUser(src *User) *domain.User {
|
||||||
|
now := time.Now()
|
||||||
|
res := &domain.User{
|
||||||
|
Identifier: domain.UserIdentifier(src.Identifier),
|
||||||
|
Email: src.Email,
|
||||||
|
Source: domain.UserSource(src.Source),
|
||||||
|
ProviderName: src.ProviderName,
|
||||||
|
IsAdmin: src.IsAdmin,
|
||||||
|
Firstname: src.Firstname,
|
||||||
|
Lastname: src.Lastname,
|
||||||
|
Phone: src.Phone,
|
||||||
|
Department: src.Department,
|
||||||
|
Notes: src.Notes,
|
||||||
|
Password: domain.PrivateString(src.Password),
|
||||||
|
Disabled: nil, // set below
|
||||||
|
DisabledReason: src.DisabledReason,
|
||||||
|
Locked: nil, // set below
|
||||||
|
LockedReason: src.LockedReason,
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.ApiToken != "" {
|
||||||
|
res.ApiToken = src.ApiToken
|
||||||
|
res.ApiTokenCreated = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Disabled {
|
||||||
|
res.Disabled = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
if src.Locked {
|
||||||
|
res.Locked = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
@ -22,9 +22,19 @@ type App struct {
|
|||||||
StatisticsCollector
|
StatisticsCollector
|
||||||
ConfigFileManager
|
ConfigFileManager
|
||||||
MailManager
|
MailManager
|
||||||
|
ApiV1Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config, bus evbus.MessageBus, authenticator Authenticator, users UserManager, wireGuard WireGuardManager, stats StatisticsCollector, cfgFiles ConfigFileManager, mailer MailManager) (*App, error) {
|
func New(
|
||||||
|
cfg *config.Config,
|
||||||
|
bus evbus.MessageBus,
|
||||||
|
authenticator Authenticator,
|
||||||
|
users UserManager,
|
||||||
|
wireGuard WireGuardManager,
|
||||||
|
stats StatisticsCollector,
|
||||||
|
cfgFiles ConfigFileManager,
|
||||||
|
mailer MailManager,
|
||||||
|
) (*App, error) {
|
||||||
|
|
||||||
a := &App{
|
a := &App{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
@ -60,7 +70,7 @@ func New(cfg *config.Config, bus evbus.MessageBus, authenticator Authenticator,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Startup(ctx context.Context) error {
|
func (a *App) Startup(ctx context.Context) error {
|
||||||
|
|
||||||
a.UserManager.StartBackgroundJobs(ctx)
|
a.UserManager.StartBackgroundJobs(ctx)
|
||||||
a.StatisticsCollector.StartBackgroundJobs(ctx)
|
a.StatisticsCollector.StartBackgroundJobs(ctx)
|
||||||
a.WireGuardManager.StartBackgroundJobs(ctx)
|
a.WireGuardManager.StartBackgroundJobs(ctx)
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user