mirror of
https://github.com/h44z/wg-portal.git
synced 2025-06-28 01:07:03 +00:00
Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
dd28a8dddf | ||
|
f994700caf | ||
|
be29abd29a | ||
|
94785c10ec | ||
|
3a732fd3e5 | ||
|
f0be66aea4 | ||
|
cbf8c5bca9 | ||
|
b6bfa1f6de | ||
|
0c8d6223ce | ||
|
e3b65ca337 | ||
|
61d8aa6589 | ||
|
7fd2bbad02 | ||
|
75a5f3d815 | ||
|
e9005b1b90 | ||
|
8816165260 | ||
|
ab9995350f | ||
|
7df4e4b813 | ||
|
657c4307b3 | ||
|
b918fb6522 | ||
|
78deede360 | ||
|
a8fb4365cf | ||
|
1394be2341 | ||
|
0102588d23 | ||
|
6a96925be7 | ||
|
f018babca7 | ||
|
c6253e7c15 | ||
|
2a1d82251e | ||
|
99d6ce73ad | ||
|
3eb84f0ee9 | ||
|
d8a57edef9 |
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@ -28,3 +28,8 @@ updates:
|
|||||||
patch:
|
patch:
|
||||||
update-types:
|
update-types:
|
||||||
- patch
|
- patch
|
||||||
|
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
6
.github/workflows/docker-publish.yml
vendored
6
.github/workflows/docker-publish.yml
vendored
@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
push:
|
push:
|
||||||
branches: [master, stable, legacy]
|
branches: [master]
|
||||||
# Publish vX.X.X tags as releases.
|
# Publish vX.X.X tags as releases.
|
||||||
tags: ["v*.*.*"]
|
tags: ["v*.*.*"]
|
||||||
|
|
||||||
@ -64,12 +64,12 @@ jobs:
|
|||||||
# major and major.minor tags are not available for alpha or beta releases
|
# major and major.minor tags are not available for alpha or beta releases
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
|
type=semver,pattern=v{{major}}.{{minor}}
|
||||||
|
type=semver,pattern=v{{major}}
|
||||||
# add v{{major}} tag, even for beta or release-canidate releases
|
# add v{{major}} tag, even for beta or release-canidate releases
|
||||||
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||||
# add {{major}} tag, even for beta releases or release-canidate releases
|
# add {{major}} tag, even for beta releases or release-canidate releases
|
||||||
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||||
# set latest tag for default branch
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
|
7
.github/workflows/pages.yml
vendored
7
.github/workflows/pages.yml
vendored
@ -27,6 +27,13 @@ jobs:
|
|||||||
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
||||||
|
|
||||||
- name: Publish documentation
|
- name: Publish documentation
|
||||||
|
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
|
||||||
|
run: mike deploy --push ${{ github.ref_name }}
|
||||||
|
env:
|
||||||
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
||||||
|
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
- name: Publish latest documentation
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
||||||
env:
|
env:
|
||||||
GIT_COMMITTER_NAME: "github-actions[bot]"
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
||||||
|
@ -50,7 +50,7 @@ COPY --from=builder /build/dist/wg-portal /
|
|||||||
######
|
######
|
||||||
# Final image
|
# Final image
|
||||||
######
|
######
|
||||||
FROM alpine:3.19
|
FROM alpine:3.22
|
||||||
# Install OS-level dependencies
|
# Install OS-level dependencies
|
||||||
RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools
|
RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools
|
||||||
# Setup timezone
|
# Setup timezone
|
||||||
|
@ -27,7 +27,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL, or Pos
|
|||||||
* Sends email to the client with QR-code and client config
|
* Sends email to the client with QR-code and client config
|
||||||
* Enable / Disable clients seamlessly
|
* Enable / Disable clients seamlessly
|
||||||
* Generation of wg-quick configuration file (`wgX.conf`) if required
|
* Generation of wg-quick configuration file (`wgX.conf`) if required
|
||||||
* User authentication (database, OAuth, or LDAP)
|
* User authentication (database, OAuth, or LDAP), Passkey support
|
||||||
* IPv6 ready
|
* IPv6 ready
|
||||||
* Docker ready
|
* Docker ready
|
||||||
* Can be used with existing WireGuard setups
|
* Can be used with existing WireGuard setups
|
||||||
|
@ -88,6 +88,9 @@ func main() {
|
|||||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
|
webAuthn, err := auth.NewWebAuthnAuthenticator(cfg, eventBus, userManager)
|
||||||
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
|
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
wireGuardManager.StartBackgroundJobs(ctx)
|
wireGuardManager.StartBackgroundJobs(ctx)
|
||||||
@ -124,7 +127,8 @@ func main() {
|
|||||||
apiV0BackendInterfaces := backendV0.NewInterfaceService(cfg, wireGuardManager, cfgFileManager)
|
apiV0BackendInterfaces := backendV0.NewInterfaceService(cfg, wireGuardManager, cfgFileManager)
|
||||||
apiV0BackendPeers := backendV0.NewPeerService(cfg, wireGuardManager, cfgFileManager, mailManager)
|
apiV0BackendPeers := backendV0.NewPeerService(cfg, wireGuardManager, cfgFileManager, mailManager)
|
||||||
|
|
||||||
apiV0EndpointAuth := handlersV0.NewAuthEndpoint(cfg, apiV0Auth, apiV0Session, validatorManager, authenticator)
|
apiV0EndpointAuth := handlersV0.NewAuthEndpoint(cfg, apiV0Auth, apiV0Session, validatorManager, authenticator,
|
||||||
|
webAuthn)
|
||||||
apiV0EndpointAudit := handlersV0.NewAuditEndpoint(cfg, apiV0Auth, auditManager)
|
apiV0EndpointAudit := handlersV0.NewAuditEndpoint(cfg, apiV0Auth, auditManager)
|
||||||
apiV0EndpointUsers := handlersV0.NewUserEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendUsers)
|
apiV0EndpointUsers := handlersV0.NewUserEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendUsers)
|
||||||
apiV0EndpointInterfaces := handlersV0.NewInterfaceEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendInterfaces)
|
apiV0EndpointInterfaces := handlersV0.NewInterfaceEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendInterfaces)
|
||||||
|
BIN
docs/assets/images/interface_view.png
Normal file
BIN
docs/assets/images/interface_view.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 134 KiB |
BIN
docs/assets/images/landing_page.png
Normal file
BIN
docs/assets/images/landing_page.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
BIN
docs/assets/images/passkey_setup.png
Normal file
BIN
docs/assets/images/passkey_setup.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
@ -32,6 +32,10 @@ database:
|
|||||||
type: sqlite
|
type: sqlite
|
||||||
dsn: data/sqlite.db
|
dsn: data/sqlite.db
|
||||||
encryption_passphrase: change-this-s3cr3t-encryption-passphrase
|
encryption_passphrase: change-this-s3cr3t-encryption-passphrase
|
||||||
|
|
||||||
|
auth:
|
||||||
|
webauthn:
|
||||||
|
enabled: true
|
||||||
```
|
```
|
||||||
|
|
||||||
## LDAP Authentication and Synchronization
|
## LDAP Authentication and Synchronization
|
||||||
@ -72,7 +76,8 @@ auth:
|
|||||||
|
|
||||||
auth:
|
auth:
|
||||||
oidc:
|
oidc:
|
||||||
# a sample Entra ID provider with environment variable substitution
|
# A sample Entra ID provider with environment variable substitution.
|
||||||
|
# Only users with an @outlook.com email address are allowed to register or login.
|
||||||
- id: azure
|
- id: azure
|
||||||
provider_name: azure
|
provider_name: azure
|
||||||
display_name: Login with</br>Entra ID
|
display_name: Login with</br>Entra ID
|
||||||
@ -80,6 +85,8 @@ auth:
|
|||||||
base_url: "https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0"
|
base_url: "https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0"
|
||||||
client_id: "${AZURE_CLIENT_ID}"
|
client_id: "${AZURE_CLIENT_ID}"
|
||||||
client_secret: "${AZURE_CLIENT_SECRET}"
|
client_secret: "${AZURE_CLIENT_SECRET}"
|
||||||
|
allowed_domains:
|
||||||
|
- "outlook.com"
|
||||||
extra_scopes:
|
extra_scopes:
|
||||||
- profile
|
- profile
|
||||||
- email
|
- email
|
||||||
|
@ -14,7 +14,7 @@ Configuration examples are available on the [Examples](./examples.md) page.
|
|||||||
```yaml
|
```yaml
|
||||||
core:
|
core:
|
||||||
admin_user: admin@wgportal.local
|
admin_user: admin@wgportal.local
|
||||||
admin_password: wgportal
|
admin_password: wgportal-default
|
||||||
admin_api_token: ""
|
admin_api_token: ""
|
||||||
editable_keys: true
|
editable_keys: true
|
||||||
create_default_peer: false
|
create_default_peer: false
|
||||||
@ -38,6 +38,7 @@ advanced:
|
|||||||
rule_prio_offset: 20000
|
rule_prio_offset: 20000
|
||||||
route_table_offset: 20000
|
route_table_offset: 20000
|
||||||
api_admin_only: true
|
api_admin_only: true
|
||||||
|
limit_additional_user_peers: 0
|
||||||
|
|
||||||
database:
|
database:
|
||||||
debug: false
|
debug: false
|
||||||
@ -72,6 +73,10 @@ auth:
|
|||||||
oidc: []
|
oidc: []
|
||||||
oauth: []
|
oauth: []
|
||||||
ldap: []
|
ldap: []
|
||||||
|
webauthn:
|
||||||
|
enabled: true
|
||||||
|
min_password_length: 16
|
||||||
|
hide_login_form: false
|
||||||
|
|
||||||
web:
|
web:
|
||||||
listening_address: :8888
|
listening_address: :8888
|
||||||
@ -118,8 +123,9 @@ More advanced options are found in the subsequent `Advanced` section.
|
|||||||
- **Description:** The administrator user. This user will be created as a default admin if it does not yet exist.
|
- **Description:** The administrator user. This user will be created as a default admin if it does not yet exist.
|
||||||
|
|
||||||
### `admin_password`
|
### `admin_password`
|
||||||
- **Default:** `wgportal`
|
- **Default:** `wgportal-default`
|
||||||
- **Description:** The administrator password. The default password of `wgportal` should be changed immediately.
|
- **Description:** The administrator password. The default password should be changed immediately!
|
||||||
|
- **Important:** The password should be strong and secure. The minimum password length is specified in [auth.min_password_length](#min_password_length). By default, it is 16 characters.
|
||||||
|
|
||||||
### `admin_api_token`
|
### `admin_api_token`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
@ -211,6 +217,10 @@ Additional or more specialized configuration options for logging and interface c
|
|||||||
- **Default:** `true`
|
- **Default:** `true`
|
||||||
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
|
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
|
||||||
|
|
||||||
|
### `limit_additional_user_peers`
|
||||||
|
- **Default:** `0`
|
||||||
|
- **Description:** Limit additional peers a normal user can create. `0` means unlimited.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
@ -334,9 +344,23 @@ Options for configuring email notifications or sending peer configurations via e
|
|||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
|
|
||||||
WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), and **LDAP** (`ldap`).
|
WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), **Passkeys** (`webauthn`) and **LDAP** (`ldap`).
|
||||||
Each can have multiple providers configured. Below are the relevant keys.
|
Each can have multiple providers configured. Below are the relevant keys.
|
||||||
|
|
||||||
|
Some core authentication options are shared across all providers, while others are specific to each provider type.
|
||||||
|
|
||||||
|
### `min_password_length`
|
||||||
|
- **Default:** `16`
|
||||||
|
- **Description:** Minimum password length for local authentication. This is not enforced for LDAP authentication.
|
||||||
|
The default admin password strength is also enforced by this setting.
|
||||||
|
- **Important:** The password should be strong and secure. It is recommended to use a password with at least 16 characters, including uppercase and lowercase letters, numbers, and special characters.
|
||||||
|
|
||||||
|
### `hide_login_form`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If `true`, the login form is hidden and only the OIDC, OAuth, LDAP, or WebAuthn providers are shown. This is useful if you want to enforce a specific authentication method.
|
||||||
|
If no social login providers are configured, the login form is always shown, regardless of this setting.
|
||||||
|
- **Important:** You can still access the login form by adding the `?all` query parameter to the login URL (e.g. https://wg.portal/#/login?all).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### OIDC
|
### OIDC
|
||||||
@ -368,6 +392,10 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
|||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** A list of additional OIDC scopes (e.g., `profile`, `email`).
|
- **Description:** A list of additional OIDC scopes (e.g., `profile`, `email`).
|
||||||
|
|
||||||
|
#### `allowed_domains`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
||||||
|
|
||||||
#### `field_map`
|
#### `field_map`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
|
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
|
||||||
@ -437,6 +465,10 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
|||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** A list of OAuth scopes.
|
- **Description:** A list of OAuth scopes.
|
||||||
|
|
||||||
|
#### `allowed_domains`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
||||||
|
|
||||||
#### `field_map`
|
#### `field_map`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
|
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
|
||||||
@ -532,6 +564,8 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
|||||||
```text
|
```text
|
||||||
(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||||
```
|
```
|
||||||
|
- **Important**: The `login_filter` must always be a valid LDAP filter. It should at most return one user.
|
||||||
|
If the filter returns multiple or no users, the login will fail.
|
||||||
|
|
||||||
#### `admin_group`
|
#### `admin_group`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
@ -572,6 +606,16 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### WebAuthn (Passkeys)
|
||||||
|
|
||||||
|
The `webauthn` section contains configuration options for WebAuthn authentication (passkeys).
|
||||||
|
|
||||||
|
#### `enabled`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** If `true`, Passkey authentication is enabled. If `false`, WebAuthn is disabled.
|
||||||
|
Users are encouraged to use Passkeys for secure authentication instead of passwords.
|
||||||
|
If a passkey is registered, the password login is still available as a fallback. Ensure that the password is strong and secure.
|
||||||
|
|
||||||
## Web
|
## Web
|
||||||
|
|
||||||
The web section contains configuration options for the web server, including the listening address, session management, and CSRF protection.
|
The web section contains configuration options for the web server, including the listening address, session management, and CSRF protection.
|
||||||
@ -632,7 +676,7 @@ The webhook section allows you to configure a webhook that is called on certain
|
|||||||
A JSON object is sent in a POST request to the webhook URL with the following structure:
|
A JSON object is sent in a POST request to the webhook URL with the following structure:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"event": "peer_created",
|
"event": "update",
|
||||||
"entity": "peer",
|
"entity": "peer",
|
||||||
"identifier": "the-peer-identifier",
|
"identifier": "the-peer-identifier",
|
||||||
"payload": {
|
"payload": {
|
||||||
@ -642,6 +686,8 @@ A JSON object is sent in a POST request to the webhook URL with the following st
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Further details can be found in the [usage documentation](../usage/webhooks.md).
|
||||||
|
|
||||||
### `url`
|
### `url`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** The POST endpoint to which the webhook is sent. The URL must be reachable from the WireGuard Portal server. If the URL is empty, the webhook is disabled.
|
- **Description:** The POST endpoint to which the webhook is sent. The URL must be reachable from the WireGuard Portal server. If the URL is empty, the webhook is disabled.
|
||||||
|
@ -110,31 +110,33 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
|
|||||||
## Image Versioning
|
## Image Versioning
|
||||||
|
|
||||||
All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal) or in the [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
|
All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal) or in the [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
|
||||||
|
|
||||||
|
Version **2** is the current stable release. Version **1** has moved to legacy status and is no longer recommended.
|
||||||
|
|
||||||
There are three types of tags in the repository:
|
There are three types of tags in the repository:
|
||||||
|
|
||||||
#### Semantic versioned tags
|
#### Semantic versioned tags
|
||||||
|
|
||||||
For example, `2.0.0-rc.1` or `v2.0.0-rc.1`.
|
For example, `2.0.0-rc.1` or `v2.0.0-rc.1`.
|
||||||
|
|
||||||
These are official releases of WireGuard Portal. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases).
|
These are official releases of WireGuard Portal. For production deployments of WireGuard Portal, we strongly recommend using one of these versioned tags instead of the latest or canary tags.
|
||||||
|
|
||||||
Once these tags show up in this repository, they will never change.
|
There are different types of these tags:
|
||||||
|
|
||||||
For production deployments of WireGuard Portal, we strongly recommend using one of these tags, e.g. `wgportal/wg-portal:2.0.0`, instead of the latest or canary tags.
|
- Major version tags: `v2` or `2`. These tags always refer to the latest image for WireGuard Portal version **2**.
|
||||||
|
- Minor version tags: `v2.x` or `2.0`. These tags always refer to the latest image for WireGuard Portal version **2.x**.
|
||||||
|
- Specific version tags (patch version): `v2.0.0` or `2.0.0`. These tags denote a very specific release. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases). Once these tags for a specific version show up in the Docker repository, they will never change.
|
||||||
|
|
||||||
If you only want to stay at the same major or major+minor version, use either `v[MAJOR]` or `[MAJOR].[MINOR]` tags. For example `v2` or `2.0`.
|
#### The `latest` tag
|
||||||
|
|
||||||
Version **2** is the current stable release. Version **1** has moved to legacy status and is no longer recommended.
|
The lastest tag is the latest stable release of WireGuard Portal. For version **2**, this is the same as the `v2` tag.
|
||||||
|
|
||||||
#### latest
|
#### The `master` tag
|
||||||
|
|
||||||
This is the most recent build to master! It changes a lot and is very unstable.
|
This is the most recent build to the main branch! It changes a lot and is very unstable.
|
||||||
|
|
||||||
We recommend that you don't use it except for development purposes.
|
We recommend that you don't use it except for development purposes or to test the latest features.
|
||||||
|
|
||||||
#### Branch tags
|
|
||||||
|
|
||||||
For each commit in the master and the stable branch, a corresponding Docker image is build. These images use the `master` or `stable` tags.
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
@ -692,6 +692,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Interfaces
|
- Interfaces
|
||||||
put:
|
put:
|
||||||
|
description: This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
|
||||||
operationId: interfaces_handleUpdatePut
|
operationId: interfaces_handleUpdatePut
|
||||||
parameters:
|
parameters:
|
||||||
- description: The interface identifier.
|
- description: The interface identifier.
|
||||||
@ -739,6 +740,7 @@ paths:
|
|||||||
- Interfaces
|
- Interfaces
|
||||||
/interface/new:
|
/interface/new:
|
||||||
post:
|
post:
|
||||||
|
description: This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
|
||||||
operationId: interfaces_handleCreatePost
|
operationId: interfaces_handleCreatePost
|
||||||
parameters:
|
parameters:
|
||||||
- description: The interface data.
|
- description: The interface data.
|
||||||
@ -779,6 +781,34 @@ paths:
|
|||||||
summary: Create a new interface record.
|
summary: Create a new interface record.
|
||||||
tags:
|
tags:
|
||||||
- Interfaces
|
- Interfaces
|
||||||
|
/interface/prepare:
|
||||||
|
get:
|
||||||
|
description: This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).
|
||||||
|
operationId: interfaces_handlePrepareGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Interface'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
security:
|
||||||
|
- BasicAuth: []
|
||||||
|
summary: Prepare a new interface record.
|
||||||
|
tags:
|
||||||
|
- Interfaces
|
||||||
/metrics/by-interface/{id}:
|
/metrics/by-interface/{id}:
|
||||||
get:
|
get:
|
||||||
operationId: metrics_handleMetricsForInterfaceGet
|
operationId: metrics_handleMetricsForInterfaceGet
|
||||||
@ -967,7 +997,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Peers
|
- Peers
|
||||||
put:
|
put:
|
||||||
description: Only admins can update existing records.
|
description: Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).
|
||||||
operationId: peers_handleUpdatePut
|
operationId: peers_handleUpdatePut
|
||||||
parameters:
|
parameters:
|
||||||
- description: The peer identifier.
|
- description: The peer identifier.
|
||||||
@ -1078,7 +1108,7 @@ paths:
|
|||||||
- Peers
|
- Peers
|
||||||
/peer/new:
|
/peer/new:
|
||||||
post:
|
post:
|
||||||
description: Only admins can create new records.
|
description: Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).
|
||||||
operationId: peers_handleCreatePost
|
operationId: peers_handleCreatePost
|
||||||
parameters:
|
parameters:
|
||||||
- description: The peer data.
|
- description: The peer data.
|
||||||
@ -1119,6 +1149,48 @@ paths:
|
|||||||
summary: Create a new peer record.
|
summary: Create a new peer record.
|
||||||
tags:
|
tags:
|
||||||
- Peers
|
- Peers
|
||||||
|
/peer/prepare/{id}:
|
||||||
|
get:
|
||||||
|
description: This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.
|
||||||
|
operationId: peers_handlePrepareGet
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Peer'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
security:
|
||||||
|
- BasicAuth: []
|
||||||
|
summary: Prepare a new peer record for the given WireGuard interface.
|
||||||
|
tags:
|
||||||
|
- Peers
|
||||||
/provisioning/data/peer-config:
|
/provisioning/data/peer-config:
|
||||||
get:
|
get:
|
||||||
description: Normal users can only access their own record. Admins can access all records.
|
description: Normal users can only access their own record. Admins can access all records.
|
||||||
|
57
docs/documentation/usage/general.md
Normal file
57
docs/documentation/usage/general.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
This documentation section describes the general usage of WireGuard Portal.
|
||||||
|
If you are looking for specific setup instructions, please refer to the *Getting Started* and [*Configuration*](../configuration/overview.md) sections,
|
||||||
|
for example, using a [Docker](../getting-started/docker.md) deployment.
|
||||||
|
|
||||||
|
## Basic Concepts
|
||||||
|
|
||||||
|
WireGuard Portal is a web-based configuration portal for WireGuard server management. It allows managing multiple WireGuard interfaces and users from a single web UI.
|
||||||
|
WireGuard Interfaces can be categorized into three types:
|
||||||
|
|
||||||
|
- **Server**: A WireGuard server interface that to which multiple peers can connect. In this mode, it is possible to specify default settings for all peers, such as the IP address range, DNS servers, and MTU size.
|
||||||
|
- **Client**: A WireGuard client interface that can be used to connect to a WireGuard server. Usually, such an interface has exactly one peer.
|
||||||
|
- **Unknown**: This is the default type for imported interfaces. It is encouraged to change the type to either `Server` or `Client` after importing the interface.
|
||||||
|
|
||||||
|
## Accessing the Web UI
|
||||||
|
|
||||||
|
The web UI should be accessed via the URL specified in the `external_url` property of the configuration file.
|
||||||
|
By default, WireGuard Portal listens on port `8888` for HTTP connections. Check the [Security](security.md) section for more information on securing the web UI.
|
||||||
|
|
||||||
|
So the default URL to access the web UI is:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8888
|
||||||
|
```
|
||||||
|
|
||||||
|
A freshly set-up WireGuard Portal instance will have a default admin user with the username `admin@wgportal.local` and the password `wgportal-default`.
|
||||||
|
You can and should override the default credentials in the configuration file. Make sure to change the default password immediately after the first login!
|
||||||
|
|
||||||
|
|
||||||
|
### Basic UI Description
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
As seen in the screenshot above, the web UI is divided into several sections which are accessible via the navigation bar on the top of the screen.
|
||||||
|
|
||||||
|
1. **Home**: The landing page of WireGuard Portal. It provides a staring point for the user to access the different sections of the web UI. It also provides quick links to WireGuard Client downloads or official documentation.
|
||||||
|
2. **Interfaces**: This section allows you to manage the WireGuard interfaces. You can add, edit, or delete interfaces, as well as view their status and statistics. Peers for each interface can be managed here as well.
|
||||||
|
3. **Users**: This section allows you to manage the users of WireGuard Portal. You can add, edit, or delete users, as well as view their status and statistics.
|
||||||
|
4. **Key Generator**: This section allows you to generate WireGuard keys locally on your browser. The generated keys are never sent to the server. This is useful if you want to generate keys for a new peer without having to store the private keys in the database.
|
||||||
|
5. **Profile / Settings**: This section allows you to access your own profile page, settings, and audit logs.
|
||||||
|
|
||||||
|
|
||||||
|
### Interface View
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The interface view provides an overview of the WireGuard interfaces and peers configured in WireGuard Portal.
|
||||||
|
|
||||||
|
The most important elements are:
|
||||||
|
|
||||||
|
1. **Interface Selector**: This dropdown allows you to select the WireGuard interface you want to manage.
|
||||||
|
All further actions will be performed on the selected interface.
|
||||||
|
2. **Create new Interface**: This button allows you to create a new WireGuard interface.
|
||||||
|
3. **Interface Overview**: This section provides an overview of the selected WireGuard interface. It shows the interface type, number of peers, and other important information.
|
||||||
|
4. **List of Peers**: This section provides a list of all peers associated with the selected WireGuard interface. You can view, add, edit, or delete peers from this list.
|
||||||
|
5. **Add new Peer**: This button allows you to add a new peer to the selected WireGuard interface.
|
||||||
|
6. **Add multiple Peers**: This button allows you to add multiple peers to the selected WireGuard interface.
|
||||||
|
This is useful if you want to add a large number of peers at once.
|
37
docs/documentation/usage/ldap.md
Normal file
37
docs/documentation/usage/ldap.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
WireGuard Portal lets you hook up any LDAP server such as Active Directory or OpenLDAP for both authentication and user sync.
|
||||||
|
You can even register multiple LDAP servers side-by-side. When someone logs in via LDAP, their specific provider is remembered,
|
||||||
|
so there's no risk of cross-provider conflicts. Details on the log-in process can be found in the [Security](security.md#ldap-authentication) documentation.
|
||||||
|
|
||||||
|
If you enable LDAP synchronization, all users within the LDAP directory will be created automatically in the WireGuard Portal database if they do not exist.
|
||||||
|
If a user is disabled or deleted in LDAP, the user will be disabled in WireGuard Portal as well.
|
||||||
|
The synchronization process can be fine-tuned by multiple parameters, which are described below.
|
||||||
|
|
||||||
|
## LDAP Synchronization
|
||||||
|
|
||||||
|
WireGuard Portal can automatically synchronize users from LDAP to the database.
|
||||||
|
To enable this feature, set the `sync_interval` property in the LDAP provider configuration to a value greater than "0".
|
||||||
|
The value is a string representing a duration, such as "15m" for 15 minutes or "1h" for 1 hour (check the [exact format definition](https://pkg.go.dev/time#ParseDuration) for details).
|
||||||
|
The synchronization process will run in the background and synchronize users from LDAP to the database at the specified interval.
|
||||||
|
Also make sure that the `sync_filter` property is a well-formed LDAP filter, or synchronization will fail.
|
||||||
|
|
||||||
|
### Limiting Synchronization to Specific Users
|
||||||
|
|
||||||
|
Use the `sync_filter` property in your LDAP provider block to restrict which users get synchronized.
|
||||||
|
It accepts any valid LDAP search filter, only entries matching that filter will be pulled into the portal's database.
|
||||||
|
|
||||||
|
For example, to import only users with a `mail` attribute:
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
ldap:
|
||||||
|
- id: ldap
|
||||||
|
# ... other settings
|
||||||
|
sync_filter: (mail=*)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disable Missing Users
|
||||||
|
|
||||||
|
If you set the `disable_missing` property to `true`, any user that is not found in LDAP during synchronization will be disabled in WireGuard Portal.
|
||||||
|
All peers associated with that user will also be disabled.
|
||||||
|
|
||||||
|
If you want a user and its peers to be automatically re-enabled once they are found in LDAP again, set the `auto_re_enable` property to `true`.
|
||||||
|
This will only re-enable the user if they where disabled by the synchronization process. Manually disabled users will not be re-enabled.
|
160
docs/documentation/usage/security.md
Normal file
160
docs/documentation/usage/security.md
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
This section describes the security features available to administrators for hardening WireGuard Portal and protecting its data.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
WireGuard Portal supports multiple authentication methods, including:
|
||||||
|
|
||||||
|
- Local user accounts
|
||||||
|
- LDAP authentication
|
||||||
|
- OAuth and OIDC authentication
|
||||||
|
- Passkey authentication (WebAuthn)
|
||||||
|
|
||||||
|
Users can have two roles which limit their permissions in WireGuard Portal:
|
||||||
|
|
||||||
|
- **User**: Can manage their own account and peers.
|
||||||
|
- **Admin**: Can manage all users and peers, including the ability to manage WireGuard interfaces.
|
||||||
|
|
||||||
|
### Password Security
|
||||||
|
|
||||||
|
WireGuard Portal supports username and password authentication for both local and LDAP-backed accounts.
|
||||||
|
Local users are stored in the database, while LDAP users are authenticated against an external LDAP server.
|
||||||
|
|
||||||
|
On initial startup, WireGuard Portal automatically creates a local admin account with the password `wgportal-default`.
|
||||||
|
> :warning: This password must be changed immediately after the first login.
|
||||||
|
|
||||||
|
The minimum password length for all local users can be configured in the [`auth`](../configuration/overview.md#auth)
|
||||||
|
section of the configuration file. The default value is **16** characters, see [`min_password_length`](../configuration/overview.md#min_password_length).
|
||||||
|
The minimum password length is also enforced for the default admin user.
|
||||||
|
|
||||||
|
|
||||||
|
### Passkey (WebAuthn) Authentication
|
||||||
|
|
||||||
|
Besides the standard authentication mechanisms, WireGuard Portal supports Passkey authentication.
|
||||||
|
This feature is enabled by default and can be configured in the [`webauthn`](../configuration/overview.md#webauthn-passkeys) section of the configuration file.
|
||||||
|
|
||||||
|
Users can register multiple Passkeys to their account. These Passkeys can be used to log in to the web UI as long as the user is not locked.
|
||||||
|
> :warning: Passkey authentication does not disable password authentication. The password can still be used to log in (e.g., as a fallback).
|
||||||
|
|
||||||
|
To register a Passkey, open the settings page *(1)* in the web UI and click on the "Register Passkey" *(2)* button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### OAuth and OIDC Authentication
|
||||||
|
|
||||||
|
WireGuard Portal supports OAuth and OIDC authentication. You can use any OAuth or OIDC provider that supports the authorization code flow,
|
||||||
|
such as Google, GitHub, or Keycloak.
|
||||||
|
|
||||||
|
For OAuth or OIDC to work, you need to configure the [`external_url`](../configuration/overview.md#external_url) property in the [`web`](../configuration/overview.md#web) section of the configuration file.
|
||||||
|
If you are planning to expose the portal to the internet, make sure that the `external_url` is configured to use HTTPS.
|
||||||
|
|
||||||
|
To add OIDC or OAuth authentication to WireGuard Portal, create a Client-ID and Client-Secret in your OAuth provider and
|
||||||
|
configure a new authentication provider in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
|
||||||
|
Make sure that each configured provider has a unique `provider_name` property set. Samples can be seen [here](../configuration/examples.md).
|
||||||
|
|
||||||
|
#### Limiting Login to Specific Domains
|
||||||
|
|
||||||
|
You can limit the login to specific domains by setting the `allowed_domains` property for OAuth or OIDC providers.
|
||||||
|
This property is a comma-separated list of domains that are allowed to log in. The user's email address is checked against this list.
|
||||||
|
For example, if you want to allow only users with an email address ending in `outlook.com` to log in, set the property as follows:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
oidc:
|
||||||
|
- provider_name: "oidc1"
|
||||||
|
# ... other settings
|
||||||
|
allowed_domains:
|
||||||
|
- "outlook.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Limit Login to Existing Users
|
||||||
|
|
||||||
|
You can limit the login to existing users only by setting the `registration_enabled` property to `false` for OAuth or OIDC providers.
|
||||||
|
If registration is enabled, new users will be created in the database when they log in for the first time.
|
||||||
|
|
||||||
|
#### Admin Mapping
|
||||||
|
|
||||||
|
You can map users to admin roles based on their attributes in the OAuth or OIDC provider. To do this, set the `admin_mapping` property for the provider.
|
||||||
|
Administrative access can either be mapped by a specific attribute or by group membership.
|
||||||
|
|
||||||
|
**Attribute specific mapping** can be achieved by setting the `admin_value_regex` and the `is_admin` property.
|
||||||
|
The `admin_value_regex` property is a regular expression that is matched against the value of the `is_admin` attribute.
|
||||||
|
The user is granted admin access if the regex matches the attribute value.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
oidc:
|
||||||
|
- provider_name: "oidc1"
|
||||||
|
# ... other settings
|
||||||
|
field_map:
|
||||||
|
is_admin: "wg_admin_prop"
|
||||||
|
admin_mapping:
|
||||||
|
admin_value_regex: "^true$"
|
||||||
|
```
|
||||||
|
The example above will grant admin access to users with the `wg_admin_prop` attribute set to `true`.
|
||||||
|
|
||||||
|
**Group membership mapping** can be achieved by setting the `admin_group_regex` and `user_groups` property.
|
||||||
|
The `admin_group_regex` property is a regular expression that is matched against the group names of the user.
|
||||||
|
The user is granted admin access if the regex matches any of the group names.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
oidc:
|
||||||
|
- provider_name: "oidc1"
|
||||||
|
# ... other settings
|
||||||
|
field_map:
|
||||||
|
user_groups: "groups"
|
||||||
|
admin_mapping:
|
||||||
|
admin_group_regex: "^the-admin-group$"
|
||||||
|
```
|
||||||
|
The example above will grant admin access to users who are members of the `the-admin-group` group.
|
||||||
|
|
||||||
|
|
||||||
|
### LDAP Authentication
|
||||||
|
|
||||||
|
WireGuard Portal supports LDAP authentication. You can use any LDAP server that supports the LDAP protocol, such as Active Directory or OpenLDAP.
|
||||||
|
Multiple LDAP servers can be configured in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
|
||||||
|
WireGuard Portal remembers the authentication provider of the user and therefore avoids conflicts between multiple LDAP providers.
|
||||||
|
|
||||||
|
To configure LDAP authentication, create a new [`ldap`](../configuration/overview.md#ldap) authentication provider in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
|
||||||
|
|
||||||
|
#### Limiting Login to Specific Users
|
||||||
|
|
||||||
|
You can limit the login to specific users by setting the `login_filter` property for LDAP provider. This filter uses the LDAP search filter syntax.
|
||||||
|
The username can be inserted into the query by placing the `{{login_identifier}}` placeholder in the filter. This placeholder will then be replaced with the username entered by the user during login.
|
||||||
|
|
||||||
|
For example, if you want to allow only users with the `objectClass` attribute set to `organizationalPerson` to log in, set the property as follows:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
ldap:
|
||||||
|
- provider_name: "ldap1"
|
||||||
|
# ... other settings
|
||||||
|
login_filter: "(&(objectClass=organizationalPerson)(uid={{login_identifier}}))"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `login_filter` should always be designed to return at most one user.
|
||||||
|
|
||||||
|
#### Limit Login to Existing Users
|
||||||
|
|
||||||
|
You can limit the login to existing users only by setting the `registration_enabled` property to `false` for LDAP providers.
|
||||||
|
If registration is enabled, new users will be created in the database when they log in for the first time.
|
||||||
|
|
||||||
|
#### Admin Mapping
|
||||||
|
|
||||||
|
You can map users to admin roles based on their group membership in the LDAP server. To do this, set the `admin_group` and `memberof` property for the provider.
|
||||||
|
The `admin_group` property defines the distinguished name of the group that is allowed to log in as admin.
|
||||||
|
All groups that are listed in the `memberof` attribute of the user will be checked against this group. If one of the groups matches, the user is granted admin access.
|
||||||
|
|
||||||
|
|
||||||
|
## UI and API Access
|
||||||
|
|
||||||
|
WireGuard Portal provides a web UI and a REST API for user interaction. It is important to secure these interfaces to prevent unauthorized access and data breaches.
|
||||||
|
|
||||||
|
### HTTPS
|
||||||
|
It is recommended to use HTTPS for all communication with the portal to prevent eavesdropping.
|
||||||
|
|
||||||
|
Event though, WireGuard Portal supports HTTPS out of the box, it is recommended to use a reverse proxy like Nginx or Traefik to handle SSL termination and other security features.
|
||||||
|
A detailed explanation is available in the [Reverse Proxy](../getting-started/reverse-proxy.md) section.
|
86
docs/documentation/usage/webhooks.md
Normal file
86
docs/documentation/usage/webhooks.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
|
||||||
|
Webhooks allow WireGuard Portal to notify external services about events such as user creation, device changes, or configuration updates. This enables integration with other systems and automation workflows.
|
||||||
|
|
||||||
|
When webhooks are configured and a specified event occurs, WireGuard Portal sends an HTTP **POST** request to the configured webhook URL.
|
||||||
|
The payload contains event-specific data in JSON format.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All available configuration options for webhooks can be found in the [configuration overview](../configuration/overview.md#webhook).
|
||||||
|
|
||||||
|
A basic webhook configuration looks like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
webhook:
|
||||||
|
url: https://your-service.example.com/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
Webhooks can be secured by using a shared secret. This secret is included in the `Authorization` header of the webhook request, allowing your service to verify the authenticity of the request.
|
||||||
|
You can set the shared secret in the webhook configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
webhook:
|
||||||
|
url: https://your-service.example.com/webhook
|
||||||
|
secret: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
|
||||||
|
```
|
||||||
|
|
||||||
|
You should also make sure that your webhook endpoint is secured with HTTPS to prevent eavesdropping and tampering.
|
||||||
|
|
||||||
|
## Available Events
|
||||||
|
|
||||||
|
WireGuard Portal supports various events that can trigger webhooks. The following events are available:
|
||||||
|
|
||||||
|
- `create`: Triggered when a new entity is created.
|
||||||
|
- `update`: Triggered when an existing entity is updated.
|
||||||
|
- `delete`: Triggered when an entity is deleted.
|
||||||
|
- `connect`: Triggered when a user connects to the VPN.
|
||||||
|
- `disconnect`: Triggered when a user disconnects from the VPN.
|
||||||
|
|
||||||
|
The following entity types can trigger webhooks:
|
||||||
|
|
||||||
|
- `user`: When a WireGuard Portal user is created, updated, or deleted.
|
||||||
|
- `peer`: When a peer is created, updated, or deleted. This entity can also trigger `connect` and `disconnect` events.
|
||||||
|
- `interface`: When a device is created, updated, or deleted.
|
||||||
|
|
||||||
|
## Payload Structure
|
||||||
|
|
||||||
|
All webhook events send a JSON payload containing relevant data. The structure of the payload depends on the event type and entity involved.
|
||||||
|
A common shell structure for webhook payloads is as follows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "create",
|
||||||
|
"entity": "user",
|
||||||
|
"identifier": "the-user-identifier",
|
||||||
|
"payload": {
|
||||||
|
// The payload of the event, e.g. peer data.
|
||||||
|
// Check the API documentation for the exact structure.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Example Payload
|
||||||
|
|
||||||
|
The following payload is an example of a webhook event when a peer connects to the VPN:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "connect",
|
||||||
|
"entity": "peer",
|
||||||
|
"identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||||
|
"payload": {
|
||||||
|
"PeerId": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||||
|
"IsConnected": true,
|
||||||
|
"IsPingable": false,
|
||||||
|
"LastPing": null,
|
||||||
|
"BytesReceived": 1860,
|
||||||
|
"BytesTransmitted": 10824,
|
||||||
|
"LastHandshake": "2025-06-26T23:04:33.325216659+02:00",
|
||||||
|
"Endpoint": "10.55.66.77:33874",
|
||||||
|
"LastSessionStart": "2025-06-26T22:50:40.10221606+02:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -24,7 +24,7 @@
|
|||||||
<div id="toasts"></div>
|
<div id="toasts"></div>
|
||||||
|
|
||||||
<!-- main application -->
|
<!-- main application -->
|
||||||
<div id="app"></div>
|
<div id="app" class="d-flex flex-column flex-grow-1"></div>
|
||||||
|
|
||||||
<!-- vue teleport will add modals and dialogs here -->
|
<!-- vue teleport will add modals and dialogs here -->
|
||||||
<div id="modals"></div>
|
<div id="modals"></div>
|
||||||
|
7
frontend/package-lock.json
generated
7
frontend/package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"@kyvg/vue3-notification": "^3.4.1",
|
"@kyvg/vue3-notification": "^3.4.1",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
||||||
"bootstrap": "^5.3.5",
|
"bootstrap": "^5.3.5",
|
||||||
"bootswatch": "^5.3.5",
|
"bootswatch": "^5.3.5",
|
||||||
@ -863,6 +864,12 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@simplewebauthn/browser": {
|
||||||
|
"version": "13.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.1.0.tgz",
|
||||||
|
"integrity": "sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"@kyvg/vue3-notification": "^3.4.1",
|
"@kyvg/vue3-notification": "^3.4.1",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
||||||
"bootstrap": "^5.3.5",
|
"bootstrap": "^5.3.5",
|
||||||
"bootswatch": "^5.3.5",
|
"bootswatch": "^5.3.5",
|
||||||
|
@ -61,6 +61,26 @@ const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
|||||||
const wgVersion = ref(WGPORTAL_VERSION);
|
const wgVersion = ref(WGPORTAL_VERSION);
|
||||||
const currentYear = ref(new Date().getFullYear())
|
const currentYear = ref(new Date().getFullYear())
|
||||||
|
|
||||||
|
const userDisplayName = computed(() => {
|
||||||
|
let displayName = "Unknown";
|
||||||
|
if (auth.IsAuthenticated) {
|
||||||
|
if (auth.User.Firstname === "" && auth.User.Lastname === "") {
|
||||||
|
displayName = auth.User.Identifier;
|
||||||
|
} else if (auth.User.Firstname === "" && auth.User.Lastname !== "") {
|
||||||
|
displayName = auth.User.Lastname;
|
||||||
|
} else if (auth.User.Firstname !== "" && auth.User.Lastname === "") {
|
||||||
|
displayName = auth.User.Firstname;
|
||||||
|
} else if (auth.User.Firstname !== "" && auth.User.Lastname !== "") {
|
||||||
|
displayName = auth.User.Firstname + " " + auth.User.Lastname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad string to 20 characters so that the menu is always the same size on desktop
|
||||||
|
if (displayName.length < 20 && window.innerWidth > 992) {
|
||||||
|
displayName = displayName.padStart(20, "\u00A0");
|
||||||
|
}
|
||||||
|
return displayName;
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -93,10 +113,10 @@ const currentYear = ref(new Date().getFullYear())
|
|||||||
<div class="navbar-nav d-flex justify-content-end">
|
<div class="navbar-nav d-flex justify-content-end">
|
||||||
<div v-if="auth.IsAuthenticated" class="nav-item dropdown">
|
<div v-if="auth.IsAuthenticated" class="nav-item dropdown">
|
||||||
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown"
|
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown"
|
||||||
href="#" role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
href="#" role="button">{{ userDisplayName }}</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>
|
<RouterLink :to="{ name: 'settings' }" class="dropdown-item" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly') || settings.Setting('WebAuthnEnabled')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</RouterLink>
|
||||||
<RouterLink :to="{ name: 'audit' }" class="dropdown-item" v-if="auth.IsAdmin"><i class="fas fa-file-shield"></i> {{ $t('menu.audit') }}</RouterLink>
|
<RouterLink :to="{ name: 'audit' }" class="dropdown-item" v-if="auth.IsAdmin"><i class="fas fa-file-shield"></i> {{ $t('menu.audit') }}</RouterLink>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="auth.Logout"><i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}</a>
|
<a class="dropdown-item" href="#" @click.prevent="auth.Logout"><i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}</a>
|
||||||
@ -140,6 +160,7 @@ const currentYear = ref(new Date().getFullYear())
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer></template>
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -5,10 +5,12 @@ import {computed, ref, watch} from "vue";
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import {freshUser} from "@/helpers/models";
|
import {freshUser} from "@/helpers/models";
|
||||||
|
import {settingsStore} from "@/stores/settings";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const users = userStore()
|
const users = userStore()
|
||||||
|
const settings = settingsStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
userId: String,
|
userId: String,
|
||||||
@ -33,6 +35,30 @@ const title = computed(() => {
|
|||||||
|
|
||||||
const formData = ref(freshUser())
|
const formData = ref(freshUser())
|
||||||
|
|
||||||
|
const passwordWeak = computed(() => {
|
||||||
|
return formData.value.Password && formData.value.Password.length > 0 && formData.value.Password.length < settings.Setting('MinPasswordLength')
|
||||||
|
})
|
||||||
|
|
||||||
|
const formValid = computed(() => {
|
||||||
|
if (formData.value.Source !== 'db') {
|
||||||
|
return true // nothing to validate
|
||||||
|
}
|
||||||
|
if (props.userId !== '#NEW#' && passwordWeak.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (props.userId === '#NEW#' && (!formData.value.Password || formData.value.Password.length < 1)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (props.userId === '#NEW#' && passwordWeak.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!formData.value.Identifier || formData.value.Identifier.length < 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
|
|
||||||
watch(() => props.visible, async (newValue, oldValue) => {
|
watch(() => props.visible, async (newValue, oldValue) => {
|
||||||
@ -109,7 +135,8 @@ async function del() {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="formData.Source==='db'" class="form-group">
|
<div v-if="formData.Source==='db'" class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.user-edit.password.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.user-edit.password.label') }}</label>
|
||||||
<input v-model="formData.Password" aria-describedby="passwordHelp" class="form-control" :placeholder="$t('modals.user-edit.password.placeholder')" type="text">
|
<input v-model="formData.Password" aria-describedby="passwordHelp" class="form-control" :class="{ 'is-invalid': passwordWeak, 'is-valid': formData.Password !== '' && !passwordWeak }" :placeholder="$t('modals.user-edit.password.placeholder')" type="password">
|
||||||
|
<div class="invalid-feedback">{{ $t('modals.user-edit.password.too-weak') }}</div>
|
||||||
<small v-if="props.userId!=='#NEW#'" id="passwordHelp" class="form-text text-muted">{{ $t('modals.user-edit.password.description') }}</small>
|
<small v-if="props.userId!=='#NEW#'" id="passwordHelp" class="form-text text-muted">{{ $t('modals.user-edit.password.description') }}</small>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -168,7 +195,7 @@ async function del() {
|
|||||||
<div class="flex-fill text-start">
|
<div class="flex-fill text-start">
|
||||||
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="!formValid">{{ $t('general.save') }}</button>
|
||||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -29,7 +29,8 @@
|
|||||||
"label": "Passwort",
|
"label": "Passwort",
|
||||||
"placeholder": "Bitte geben Sie Ihr Passwort ein"
|
"placeholder": "Bitte geben Sie Ihr Passwort ein"
|
||||||
},
|
},
|
||||||
"button": "Anmelden"
|
"button": "Anmelden",
|
||||||
|
"button-webauthn": "Passkey verwenden"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
@ -188,6 +189,35 @@
|
|||||||
"button-enable-title": "Aktivieren Sie die API, dadurch wird ein neuer Token generiert.",
|
"button-enable-title": "Aktivieren Sie die API, dadurch wird ein neuer Token generiert.",
|
||||||
"button-enable-text": "API aktivieren",
|
"button-enable-text": "API aktivieren",
|
||||||
"api-link": "API Dokumentation"
|
"api-link": "API Dokumentation"
|
||||||
|
},
|
||||||
|
"webauthn": {
|
||||||
|
"headline": "Passkey-Einstellungen",
|
||||||
|
"abstract": "Passkeys sind eine moderne Möglichkeit, Benutzer ohne Passwort zu authentifizieren. Sie werden sicher in Ihrem Browser gespeichert und können verwendet werden, um sich im WireGuard-Portal anzumelden.",
|
||||||
|
"active-description": "Mindestens ein Passkey ist derzeit für Ihr Benutzerkonto aktiv.",
|
||||||
|
"inactive-description": "Für Ihr Benutzerkonto sind derzeit keine Passkeys registriert. Drücken Sie die Schaltfläche unten, um einen neuen Passkey zu registrieren.",
|
||||||
|
"table": {
|
||||||
|
"name": "Name",
|
||||||
|
"created": "Erstellt",
|
||||||
|
"actions": ""
|
||||||
|
},
|
||||||
|
"credentials-list": "Derzeit registrierte Passkeys",
|
||||||
|
"modal-delete": {
|
||||||
|
"headline": "Passkey löschen",
|
||||||
|
"abstract": "Sind Sie sicher, dass Sie diesen Passkey löschen möchten? Sie können sich anschließend nicht mehr mit diesem Passkey anmelden.",
|
||||||
|
"created": "Erstellt:",
|
||||||
|
"button-delete": "Löschen",
|
||||||
|
"button-cancel": "Abbrechen"
|
||||||
|
},
|
||||||
|
"button-rename-title": "Umbenennen",
|
||||||
|
"button-rename-text": "Passkey umbenennen.",
|
||||||
|
"button-save-title": "Speichern",
|
||||||
|
"button-save-text": "Neuen Namen des Passkeys speichern.",
|
||||||
|
"button-cancel-title": "Abbrechen",
|
||||||
|
"button-cancel-text": "Umbenennung des Passkeys abbrechen.",
|
||||||
|
"button-delete-title": "Löschen",
|
||||||
|
"button-delete-text": "Passkey löschen. Sie können sich anschließend nicht mehr mit diesem Passkey anmelden.",
|
||||||
|
"button-register-title": "Passkey registrieren",
|
||||||
|
"button-register-text": "Einen neuen Passkey registrieren, um Ihr Konto zu sichern."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"audit": {
|
"audit": {
|
||||||
@ -266,7 +296,8 @@
|
|||||||
"password": {
|
"password": {
|
||||||
"label": "Passwort",
|
"label": "Passwort",
|
||||||
"placeholder": "Ein super geheimes Passwort",
|
"placeholder": "Ein super geheimes Passwort",
|
||||||
"description": "Lassen Sie dieses Feld leer, um das aktuelle Passwort beizubehalten."
|
"description": "Lassen Sie dieses Feld leer, um das aktuelle Passwort beizubehalten.",
|
||||||
|
"too-weak": "Das Passwort entspricht nicht den Sicherheitsanforderungen."
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"label": "E-Mail",
|
"label": "E-Mail",
|
||||||
|
@ -29,7 +29,8 @@
|
|||||||
"label": "Password",
|
"label": "Password",
|
||||||
"placeholder": "Please enter your password"
|
"placeholder": "Please enter your password"
|
||||||
},
|
},
|
||||||
"button": "Sign in"
|
"button": "Sign in",
|
||||||
|
"button-webauthn": "Use Passkey"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
@ -188,6 +189,35 @@
|
|||||||
"button-enable-title": "Enable API, this will generate a new token.",
|
"button-enable-title": "Enable API, this will generate a new token.",
|
||||||
"button-enable-text": "Enable API",
|
"button-enable-text": "Enable API",
|
||||||
"api-link": "API Documentation"
|
"api-link": "API Documentation"
|
||||||
|
},
|
||||||
|
"webauthn": {
|
||||||
|
"headline": "Passkey Settings",
|
||||||
|
"abstract": "Passkeys are a modern way to authenticate users without the need for passwords. They are stored securely in your browser and can be used to log in to the WireGuard Portal.",
|
||||||
|
"active-description": "At least one passkey is currently active for your user account.",
|
||||||
|
"inactive-description": "No passkeys are currently registered for your user account. Press the button below to register a new passkey.",
|
||||||
|
"table": {
|
||||||
|
"name": "Name",
|
||||||
|
"created": "Created",
|
||||||
|
"actions": ""
|
||||||
|
},
|
||||||
|
"credentials-list": "Currently registered Passkeys",
|
||||||
|
"modal-delete": {
|
||||||
|
"headline": "Delete Passkey",
|
||||||
|
"abstract": "Are you sure you want to delete this passkey? You will not be able to log in with this passkey anymore.",
|
||||||
|
"created": "Created:",
|
||||||
|
"button-delete": "Delete",
|
||||||
|
"button-cancel": "Cancel"
|
||||||
|
},
|
||||||
|
"button-rename-title": "Rename",
|
||||||
|
"button-rename-text": "Rename the passkey.",
|
||||||
|
"button-save-title": "Save",
|
||||||
|
"button-save-text": "Save the new name of the passkey.",
|
||||||
|
"button-cancel-title": "Cancel",
|
||||||
|
"button-cancel-text": "Cancel the renaming of the passkey.",
|
||||||
|
"button-delete-title": "Delete",
|
||||||
|
"button-delete-text": "Delete the passkey. You will not be able to log in with this passkey anymore.",
|
||||||
|
"button-register-title": "Register Passkey",
|
||||||
|
"button-register-text": "Register a new Passkey to secure your account."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"audit": {
|
"audit": {
|
||||||
@ -266,7 +296,8 @@
|
|||||||
"password": {
|
"password": {
|
||||||
"label": "Password",
|
"label": "Password",
|
||||||
"placeholder": "A super secret password",
|
"placeholder": "A super secret password",
|
||||||
"description": "Leave this field blank to keep current password."
|
"description": "Leave this field blank to keep current password.",
|
||||||
|
"too-weak": "The password is too weak. Please use a stronger password."
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"label": "Email",
|
"label": "Email",
|
||||||
|
@ -20,14 +20,14 @@
|
|||||||
"delete": "Eliminar"
|
"delete": "Eliminar"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"headline": "Por favor, inicie sessão",
|
"headline": "Por favor, inicie a sessão",
|
||||||
"username": {
|
"username": {
|
||||||
"label": "Nome de utilizador",
|
"label": "Nome de utilizador",
|
||||||
"placeholder": "Introduza o seu nome de utilizador"
|
"placeholder": "Por favor, insira o seu nome de utilizador"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Palavra-passe",
|
"label": "Palavra-passe",
|
||||||
"placeholder": "Introduza a sua palavra-passe"
|
"placeholder": "Por favor, insira a sua palavra-passe"
|
||||||
},
|
},
|
||||||
"button": "Iniciar sessão"
|
"button": "Iniciar sessão"
|
||||||
},
|
},
|
||||||
@ -40,143 +40,513 @@
|
|||||||
"settings": "Definições",
|
"settings": "Definições",
|
||||||
"audit": "Registo de Auditoria",
|
"audit": "Registo de Auditoria",
|
||||||
"login": "Iniciar Sessão",
|
"login": "Iniciar Sessão",
|
||||||
"logout": "Terminar Sessão"
|
"logout": "Terminar Sessão",
|
||||||
|
"keygen": "Gerador de Chave"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Início",
|
"headline": "WireGuard® Portal VPN",
|
||||||
"card": {
|
"info-headline": "Mais Informações",
|
||||||
"interfaces": "Interfaces",
|
"abstract": "WireGuard® é uma VPN extremamente simples, mas rápida e moderna que utiliza criptografia de última geração. O seu objetivo é ser mais rápida, simples, leve e útil que o IPsec, enquanto evita grandes dores de cabeça. Pretende ser consideravelmente mais eficiente que o OpenVPN.",
|
||||||
"users": "Utilizadores"
|
"installation": {
|
||||||
|
"box-header": "Instalação do WireGuard",
|
||||||
|
"headline": "Instalação",
|
||||||
|
"content": "As instruções de instalação para o software cliente podem ser encontradas no site oficial do WireGuard.",
|
||||||
|
"button": "Abrir Instruções"
|
||||||
|
},
|
||||||
|
"about-wg": {
|
||||||
|
"box-header": "Sobre o WireGuard",
|
||||||
|
"headline": "Sobre",
|
||||||
|
"content": "WireGuard® é uma VPN extremamente simples, mas rápida e moderna que utiliza criptografia de última geração.",
|
||||||
|
"button": "Mais"
|
||||||
|
},
|
||||||
|
"about-portal": {
|
||||||
|
"box-header": "Sobre o WireGuard Portal",
|
||||||
|
"headline": "WireGuard Portal",
|
||||||
|
"content": "WireGuard Portal é um portal web de configuração simples para o WireGuard.",
|
||||||
|
"button": "Mais"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"headline": "Perfis VPN",
|
||||||
|
"abstract": "Pode aceder e baixar as suas configurações pessoais de VPN através do seu Perfil de Utilizador.",
|
||||||
|
"content": "Para encontrar todos os seus perfis configurados, clique no botão abaixo.",
|
||||||
|
"button": "Abrir meu perfil"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"headline": "Área de Administração",
|
||||||
|
"abstract": "Na área de administração, pode gerir os peers do WireGuard, a interface do servidor e os utilizadores que têm permissão para aceder ao Portal WireGuard.",
|
||||||
|
"content": "",
|
||||||
|
"button-admin": "Abrir Administração do Servidor",
|
||||||
|
"button-user": "Abrir Administração de Utilizadores"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"interfaces": {
|
"interfaces": {
|
||||||
"title": "Interfaces",
|
"headline": "Administração de Interfaces",
|
||||||
"create": "Criar Interface",
|
"headline-peers": "Peers VPN Atuais",
|
||||||
"name": "Nome",
|
"headline-endpoints": "Endpoints Atuais",
|
||||||
"address": "Endereço",
|
"no-interface": {
|
||||||
"listen-port": "Porta de Escuta",
|
"default-selection": "Nenhuma interface disponível",
|
||||||
"public-key": "Chave Pública",
|
"headline": "Nenhuma interface encontrada...",
|
||||||
"private-key": "Chave Privada",
|
"abstract": "Clique no botão + acima para criar uma nova interface WireGuard."
|
||||||
"actions": "Ações",
|
|
||||||
"delete-dialog": {
|
|
||||||
"title": "Eliminar Interface",
|
|
||||||
"text": "Tem a certeza de que deseja eliminar a interface '{{name}}'?"
|
|
||||||
},
|
},
|
||||||
"form": {
|
"no-peer": {
|
||||||
"name": {
|
"headline": "Nenhum peer disponível",
|
||||||
"label": "Nome",
|
"abstract": "Atualmente, não há peers disponíveis para a interface WireGuard selecionada."
|
||||||
"placeholder": "Introduza um nome exclusivo para a interface"
|
},
|
||||||
},
|
"table-heading": {
|
||||||
"address": {
|
"name": "Nome",
|
||||||
"label": "Endereço",
|
"user": "Utilizador",
|
||||||
"placeholder": "Introduza um endereço válido (ex: 10.0.0.1/24)"
|
"ip": "IPs",
|
||||||
},
|
"endpoint": "Endpoint",
|
||||||
"listen-port": {
|
"status": "Status"
|
||||||
"label": "Porta de Escuta",
|
},
|
||||||
"placeholder": "Introduza a porta onde o WireGuard irá escutar (ex: 51820)"
|
"interface": {
|
||||||
},
|
"headline": "Status da interface para",
|
||||||
"private-key": {
|
"mode": "modo",
|
||||||
"label": "Chave Privada",
|
"key": "Chave Pública",
|
||||||
"placeholder": "Será gerada automaticamente se não for fornecida"
|
"endpoint": "Endpoint Público",
|
||||||
}
|
"port": "Porta de Escuta",
|
||||||
}
|
"peers": "Peers Ativados",
|
||||||
|
"total-peers": "Total de Peers",
|
||||||
|
"endpoints": "Endpoints Ativados",
|
||||||
|
"total-endpoints": "Total de Endpoints",
|
||||||
|
"ip": "Endereço IP",
|
||||||
|
"default-allowed-ip": "IPs permitidos por padrão",
|
||||||
|
"dns": "Servidores DNS",
|
||||||
|
"mtu": "MTU",
|
||||||
|
"default-keep-alive": "Intervalo de Keepalive Padrão",
|
||||||
|
"button-show-config": "Mostrar configuração",
|
||||||
|
"button-download-config": "Baixar configuração",
|
||||||
|
"button-store-config": "Armazenar configuração para wg-quick",
|
||||||
|
"button-edit": "Editar interface"
|
||||||
|
},
|
||||||
|
"button-add-interface": "Adicionar Interface",
|
||||||
|
"button-add-peer": "Adicionar Peer",
|
||||||
|
"button-add-peers": "Adicionar Vários Peers",
|
||||||
|
"button-show-peer": "Mostrar Peer",
|
||||||
|
"button-edit-peer": "Editar Peer",
|
||||||
|
"peer-disabled": "Peer desativado, razão:",
|
||||||
|
"peer-expiring": "Peer expira em",
|
||||||
|
"peer-connected": "Conectado",
|
||||||
|
"peer-not-connected": "Não Conectado",
|
||||||
|
"peer-handshake": "Último handshake:"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"title": "Utilizadores",
|
"headline": "Administração de Utilizadores",
|
||||||
"create": "Criar Utilizador",
|
"table-heading": {
|
||||||
"name": "Nome",
|
"id": "ID",
|
||||||
"email": "Email",
|
"email": "E-Mail",
|
||||||
"enabled": "Ativo",
|
"firstname": "Primeiro Nome",
|
||||||
"is-admin": "Administrador",
|
"lastname": "Último Nome",
|
||||||
"actions": "Ações",
|
"source": "Fonte",
|
||||||
"edit": "Editar",
|
"peers": "Peers",
|
||||||
"delete-dialog": {
|
"admin": "Administrador"
|
||||||
"title": "Eliminar Utilizador",
|
|
||||||
"text": "Tem a certeza de que deseja eliminar o utilizador '{{nome}}'?"
|
|
||||||
},
|
},
|
||||||
"form": {
|
"no-user": {
|
||||||
"name": {
|
"headline": "Nenhum utilizador disponível",
|
||||||
"label": "Nome",
|
"abstract": "Atualmente, não há utilizadores registados no Portal WireGuard."
|
||||||
"placeholder": "Introduza o nome do utilizador"
|
},
|
||||||
|
"button-add-user": "Adicionar Utilizador",
|
||||||
|
"button-show-user": "Mostrar Utilizador",
|
||||||
|
"button-edit-user": "Editar Utilizador",
|
||||||
|
"user-disabled": "Utilizador desativado, razão:",
|
||||||
|
"user-locked": "Conta bloqueada, razão:",
|
||||||
|
"admin": "O utilizador tem privilégios de administrador",
|
||||||
|
"no-admin": "O utilizador não tem privilégios de administrador"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"headline": "Os Meus Peers VPN",
|
||||||
|
"table-heading": {
|
||||||
|
"name": "Nome",
|
||||||
|
"ip": "IPs",
|
||||||
|
"stats": "Status",
|
||||||
|
"interface": "Interface do Servidor"
|
||||||
|
},
|
||||||
|
"no-peer": {
|
||||||
|
"headline": "Nenhum peer disponível",
|
||||||
|
"abstract": "Atualmente, não há peers associados ao seu perfil de utilizador."
|
||||||
|
},
|
||||||
|
"peer-connected": "Conectado",
|
||||||
|
"button-add-peer": "Adicionar Peer",
|
||||||
|
"button-show-peer": "Mostrar Peer",
|
||||||
|
"button-edit-peer": "Editar Peer"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"headline": "Definições",
|
||||||
|
"abstract": "Aqui pode alterar suas Definições pessoais.",
|
||||||
|
"api": {
|
||||||
|
"headline": "Definições da API",
|
||||||
|
"abstract": "Aqui pode configurar as definições da API RESTful.",
|
||||||
|
"active-description": "A API está atualmente ativa para a sua conta de utilizador. Todos os pedidos para a API são autenticadas com Basic Auth. Use as seguintes credenciais para autenticação.",
|
||||||
|
"inactive-description": "A API está atualmente inativa. Pressione o botão abaixo para ativá-la.",
|
||||||
|
"user-label": "Nome de utilizador API:",
|
||||||
|
"user-placeholder": "O utilizador da API",
|
||||||
|
"token-label": "Senha da API:",
|
||||||
|
"token-placeholder": "O token da API",
|
||||||
|
"token-created-label": "Acesso API concedido em: ",
|
||||||
|
"button-disable-title": "Desativar API, invalidando o token atual.",
|
||||||
|
"button-disable-text": "Desativar API",
|
||||||
|
"button-enable-title": "Ativar API, gerando um novo token.",
|
||||||
|
"button-enable-text": "Ativar API",
|
||||||
|
"api-link": "Documentação da API"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audit": {
|
||||||
|
"headline": "Registo de Auditoria",
|
||||||
|
"abstract": "Aqui pode encontrar o registo de auditoria de todas as ações realizadas no WireGuard Portal.",
|
||||||
|
"no-entries": {
|
||||||
|
"headline": "Nenhuma entrada no registo",
|
||||||
|
"abstract": "Atualmente, não há entradas de registo de auditoria gravadas."
|
||||||
|
},
|
||||||
|
"entries-headline": "Entradas do Registo",
|
||||||
|
"table-heading": {
|
||||||
|
"id": "#",
|
||||||
|
"time": "Hora",
|
||||||
|
"user": "Utilizador",
|
||||||
|
"severity": "Gravidade",
|
||||||
|
"origin": "Origem",
|
||||||
|
"message": "Mensagem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keygen": {
|
||||||
|
"headline": "Gerador de Chaves WireGuard",
|
||||||
|
"abstract": "Gere novas chaves WireGuard. As chaves são geradas no seu browser e nunca são enviadas para o servidor.",
|
||||||
|
"headline-keypair": "Novo Par de Chaves",
|
||||||
|
"headline-preshared-key": "Nova Chave Pré-Partilhada",
|
||||||
|
"button-generate": "Gerar",
|
||||||
|
"private-key": {
|
||||||
|
"label": "Chave Privada",
|
||||||
|
"placeholder": "A chave privada"
|
||||||
|
},
|
||||||
|
"public-key": {
|
||||||
|
"label": "Chave Pública",
|
||||||
|
"placeholder": "A chave pública"
|
||||||
|
},
|
||||||
|
"preshared-key": {
|
||||||
|
"label": "Chave Pré-Partilhada",
|
||||||
|
"placeholder": "A chave pré-partilhada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modals": {
|
||||||
|
"user-view": {
|
||||||
|
"headline": "Conta de Utilizador:",
|
||||||
|
"tab-user": "Informação",
|
||||||
|
"tab-peers": "Peers",
|
||||||
|
"headline-info": "Informação do Utilizador:",
|
||||||
|
"headline-notes": "Notas:",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"firstname": "Primeiro Nome",
|
||||||
|
"lastname": "Último Nome",
|
||||||
|
"phone": "Número de Telefone",
|
||||||
|
"department": "Departamento",
|
||||||
|
"api-enabled": "Acesso API",
|
||||||
|
"disabled": "Conta Desativada",
|
||||||
|
"locked": "Conta Bloqueada",
|
||||||
|
"no-peers": "O utilizador não tem peers associados.",
|
||||||
|
"peers": {
|
||||||
|
"name": "Nome",
|
||||||
|
"interface": "Interface",
|
||||||
|
"ip": "IP's"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user-edit": {
|
||||||
|
"headline-edit": "Editar utilizador:",
|
||||||
|
"headline-new": "Novo utilizador",
|
||||||
|
"header-general": "Geral",
|
||||||
|
"header-personal": "Informação do Utilizador",
|
||||||
|
"header-notes": "Notas",
|
||||||
|
"header-state": "Estado",
|
||||||
|
"identifier": {
|
||||||
|
"label": "Identificador",
|
||||||
|
"placeholder": "O identificador único do utilizador"
|
||||||
},
|
},
|
||||||
"email": {
|
"source": {
|
||||||
"label": "Email",
|
"label": "Fonte",
|
||||||
"placeholder": "Introduza o email do utilizador"
|
"placeholder": "A fonte do utilizador"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Palavra-passe",
|
"label": "Palavra-passe",
|
||||||
"placeholder": "Deixe em branco para manter a atual"
|
"placeholder": "Uma palavra-passe super secreta",
|
||||||
|
"description": "Deixe este campo em branco para manter a palavra-passe atual."
|
||||||
},
|
},
|
||||||
"is-admin": {
|
"email": {
|
||||||
"label": "Administrador"
|
"label": "Email",
|
||||||
|
"placeholder": "O endereço de e-mail"
|
||||||
},
|
},
|
||||||
"enabled": {
|
"phone": {
|
||||||
"label": "Ativo"
|
"label": "Telefone",
|
||||||
|
"placeholder": "O número de telefone"
|
||||||
|
},
|
||||||
|
"department": {
|
||||||
|
"label": "Departamento",
|
||||||
|
"placeholder": "O departamento"
|
||||||
|
},
|
||||||
|
"firstname": {
|
||||||
|
"label": "Primeiro Nome",
|
||||||
|
"placeholder": "Primeiro Nome"
|
||||||
|
},
|
||||||
|
"lastname": {
|
||||||
|
"label": "Último Nome",
|
||||||
|
"placeholder": "Último Nome"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"label": "Notas",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"label": "Desativado (sem conexão WireGuard e login possível)"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"label": "Bloqueado (sem login possível, as conexões WireGuard ainda funcionam)"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"label": "É Administrador"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
"peers": {
|
|
||||||
"title": "Peers",
|
|
||||||
"create": "Criar Peer",
|
|
||||||
"public-key": "Chave Pública",
|
|
||||||
"preshared-key": "Chave Pré-partilhada",
|
|
||||||
"endpoint": "Endpoint",
|
|
||||||
"allowed-ips": "IPs Permitidos",
|
|
||||||
"latest-handshake": "Último Handshake",
|
|
||||||
"transfer-rx": "Recebido",
|
|
||||||
"transfer-tx": "Enviado",
|
|
||||||
"persistent-keepalive": "Keepalive Persistente",
|
|
||||||
"actions": "Ações",
|
|
||||||
"edit": "Editar",
|
|
||||||
"delete-dialog": {
|
|
||||||
"title": "Eliminar Peer",
|
|
||||||
"text": "Tem a certeza de que deseja eliminar este peer?"
|
|
||||||
},
|
},
|
||||||
"form": {
|
"interface-view": {
|
||||||
|
"headline": "Configuração para a Interface:"
|
||||||
|
},
|
||||||
|
"interface-edit": {
|
||||||
|
"headline-edit": "Editar Interface:",
|
||||||
|
"headline-new": "Nova Interface",
|
||||||
|
"tab-interface": "Interface",
|
||||||
|
"tab-peerdef": "Padrões de Peer",
|
||||||
|
"header-general": "Geral",
|
||||||
|
"header-network": "Rede",
|
||||||
|
"header-crypto": "Criptografia",
|
||||||
|
"header-hooks": "Hooks da Interface",
|
||||||
|
"header-peer-hooks": "Hooks",
|
||||||
|
"header-state": "Estado",
|
||||||
|
"identifier": {
|
||||||
|
"label": "Identificador",
|
||||||
|
"placeholder": "O identificador único da interface"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"label": "Modo da Interface",
|
||||||
|
"server": "Modo Servidor",
|
||||||
|
"client": "Modo Cliente",
|
||||||
|
"any": "Modo Desconhecido"
|
||||||
|
},
|
||||||
|
"display-name": {
|
||||||
|
"label": "Nome de Exibição",
|
||||||
|
"placeholder": "O nome descritivo para a interface"
|
||||||
|
},
|
||||||
|
"private-key": {
|
||||||
|
"label": "Chave Privada",
|
||||||
|
"placeholder": "A chave privada"
|
||||||
|
},
|
||||||
"public-key": {
|
"public-key": {
|
||||||
"label": "Chave Pública",
|
"label": "Chave Pública",
|
||||||
"placeholder": "Introduza a chave pública do peer"
|
"placeholder": "A chave pública"
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"label": "Endereços IP",
|
||||||
|
"placeholder": "Endereços IP (formato CIDR)"
|
||||||
|
},
|
||||||
|
"listen-port": {
|
||||||
|
"label": "Porta de Escuta",
|
||||||
|
"placeholder": "A porta de escuta"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"label": "Servidor DNS",
|
||||||
|
"placeholder": "Os servidores DNS que devem ser usados"
|
||||||
|
},
|
||||||
|
"dns-search": {
|
||||||
|
"label": "Domínios de Pesquisa DNS",
|
||||||
|
"placeholder": "Prefixos de pesquisa DNS"
|
||||||
|
},
|
||||||
|
"mtu": {
|
||||||
|
"label": "MTU",
|
||||||
|
"placeholder": "O MTU da interface (0 = manter o valor padrão)"
|
||||||
|
},
|
||||||
|
"firewall-mark": {
|
||||||
|
"label": "Marca de Firewall",
|
||||||
|
"placeholder": "Marca de firewall aplicada ao tráfego de saída. (0 = automático)"
|
||||||
|
},
|
||||||
|
"routing-table": {
|
||||||
|
"label": "Tabela de Roteamento",
|
||||||
|
"placeholder": "O ID da tabela de roteamento",
|
||||||
|
"description": "Casos especiais: off = não gerenciar rotas, 0 = automático"
|
||||||
|
},
|
||||||
|
"pre-up": {
|
||||||
|
"label": "Pre-Up",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"post-up": {
|
||||||
|
"label": "Post-Up",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"pre-down": {
|
||||||
|
"label": "Pre-Down",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"post-down": {
|
||||||
|
"label": "Post-Down",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"label": "Interface Desativada"
|
||||||
|
},
|
||||||
|
"save-config": {
|
||||||
|
"label": "Guardar configuração wg-quick automaticamente"
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"endpoint": {
|
||||||
|
"label": "Endereço do Endpoint",
|
||||||
|
"placeholder": "Endereço do Endpoint",
|
||||||
|
"description": "O endereço do endpoint ao qual os peers se irão conectar. (ex. wg.exemplo.com ou wg.exemplo.com:51820)"
|
||||||
|
},
|
||||||
|
"networks": {
|
||||||
|
"label": "Redes IP",
|
||||||
|
"placeholder": "Endereços de Rede",
|
||||||
|
"description": "Os peers irão obter endereços IP a partir dessas sub-redes."
|
||||||
|
},
|
||||||
|
"allowed-ip": {
|
||||||
|
"label": "Endereços IP Permitidos",
|
||||||
|
"placeholder": "Endereços IP Permitidos por padrão"
|
||||||
|
},
|
||||||
|
"mtu": {
|
||||||
|
"label": "MTU",
|
||||||
|
"placeholder": "O MTU do cliente (0 = manter o valor padrão)"
|
||||||
|
},
|
||||||
|
"keep-alive": {
|
||||||
|
"label": "Intervalo de Keep Alive",
|
||||||
|
"placeholder": "Keepalive persistente (0 = padrão)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"button-apply-defaults": "Aplicar Padrões de Peer"
|
||||||
|
},
|
||||||
|
"peer-view": {
|
||||||
|
"headline-peer": "Peer:",
|
||||||
|
"headline-endpoint": "Endpoint:",
|
||||||
|
"section-info": "Informação do Peer",
|
||||||
|
"section-status": "Estado Atual",
|
||||||
|
"section-config": "Configuração",
|
||||||
|
"identifier": "Identificador",
|
||||||
|
"ip": "Endereços IP",
|
||||||
|
"user": "Utilizador Associado",
|
||||||
|
"notes": "Notas",
|
||||||
|
"expiry-status": "Expira em",
|
||||||
|
"disabled-status": "Desativado em",
|
||||||
|
"traffic": "Tráfego",
|
||||||
|
"connection-status": "Estatísticas de Conexão",
|
||||||
|
"upload": "Bytes Enviados (do Servidor para o Peer)",
|
||||||
|
"download": "Bytes Recebidos (do Peer para o Servidor)",
|
||||||
|
"pingable": "É Pingável",
|
||||||
|
"handshake": "Último Handshake",
|
||||||
|
"connected-since": "Conectado desde",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"button-download": "Baixar configuração",
|
||||||
|
"button-email": "Enviar configuração por E-Mail"
|
||||||
|
},
|
||||||
|
"peer-edit": {
|
||||||
|
"headline-edit-peer": "Editar peer:",
|
||||||
|
"headline-edit-endpoint": "Editar endpoint:",
|
||||||
|
"headline-new-peer": "Criar peer",
|
||||||
|
"headline-new-endpoint": "Criar endpoint",
|
||||||
|
"header-general": "Geral",
|
||||||
|
"header-network": "Rede",
|
||||||
|
"header-crypto": "Criptografia",
|
||||||
|
"header-hooks": "Hooks (Executados no Peer)",
|
||||||
|
"header-state": "Estado",
|
||||||
|
"display-name": {
|
||||||
|
"label": "Nome de Exibição",
|
||||||
|
"placeholder": "O nome descritivo para o peer"
|
||||||
|
},
|
||||||
|
"linked-user": {
|
||||||
|
"label": "Utilizador Associado",
|
||||||
|
"placeholder": "A conta de utilizador que possui este peer"
|
||||||
|
},
|
||||||
|
"private-key": {
|
||||||
|
"label": "Chave Privada",
|
||||||
|
"placeholder": "A chave privada",
|
||||||
|
"help": "A chave privada é armazenada de forma segura no servidor. Se o utilizador já tiver uma cópia, pode omitir este campo. O servidor ainda funciona exclusivamente com a chave pública do peer."
|
||||||
|
},
|
||||||
|
"public-key": {
|
||||||
|
"label": "Chave Pública",
|
||||||
|
"placeholder": "A chave pública"
|
||||||
},
|
},
|
||||||
"preshared-key": {
|
"preshared-key": {
|
||||||
"label": "Chave Pré-partilhada",
|
"label": "Chave Pré-Partilhada",
|
||||||
"placeholder": "Opcional: Chave partilhada adicional para maior segurança"
|
"placeholder": "Chave pré-partilhada opcional"
|
||||||
|
},
|
||||||
|
"endpoint-public-key": {
|
||||||
|
"label": "Chave Pública do Endpoint",
|
||||||
|
"placeholder": "A chave pública do endpoint remoto"
|
||||||
},
|
},
|
||||||
"endpoint": {
|
"endpoint": {
|
||||||
"label": "Endpoint",
|
"label": "Endereço do Endpoint",
|
||||||
"placeholder": "Endereço público do peer (ex: 1.2.3.4:51820)"
|
"placeholder": "O endereço do endpoint remoto"
|
||||||
},
|
},
|
||||||
"allowed-ips": {
|
"ip": {
|
||||||
"label": "IPs Permitidos",
|
"label": "Endereços IP",
|
||||||
"placeholder": "Lista de IPs (ex: 10.0.0.2/32, 192.168.1.0/24)"
|
"placeholder": "Endereços IP (formato CIDR)"
|
||||||
},
|
},
|
||||||
"persistent-keepalive": {
|
"allowed-ip": {
|
||||||
"label": "Keepalive Persistente",
|
"label": "Endereços IP Permitidos",
|
||||||
"placeholder": "Ex: 25 (em segundos)"
|
"placeholder": "Endereços IP permitidos"
|
||||||
|
},
|
||||||
|
"extra-allowed-ip": {
|
||||||
|
"label": "Endereços IP adicionais permitidos",
|
||||||
|
"placeholder": "IPs adicionais permitidos (lado do servidor)",
|
||||||
|
"description": "Esses IPs serão adicionados à interface WireGuard remota como IPs permitidos."
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"label": "Servidor DNS",
|
||||||
|
"placeholder": "Os servidores DNS que devem ser utilizados"
|
||||||
|
},
|
||||||
|
"dns-search": {
|
||||||
|
"label": "Domínios de Pesquisa DNS",
|
||||||
|
"placeholder": "Prefixos de pesquisa DNS"
|
||||||
|
},
|
||||||
|
"keep-alive": {
|
||||||
|
"label": "Intervalo de Keep Alive",
|
||||||
|
"placeholder": "Keepalive persistente (0 = padrão)"
|
||||||
|
},
|
||||||
|
"mtu": {
|
||||||
|
"label": "MTU",
|
||||||
|
"placeholder": "O MTU do cliente (0 = manter o padrão)"
|
||||||
|
},
|
||||||
|
"pre-up": {
|
||||||
|
"label": "Pre-Up",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"post-up": {
|
||||||
|
"label": "Post-Up",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"pre-down": {
|
||||||
|
"label": "Pre-Down",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"post-down": {
|
||||||
|
"label": "Post-Down",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"label": "Peer Desativado"
|
||||||
|
},
|
||||||
|
"ignore-global": {
|
||||||
|
"label": "Ignorar definições globais"
|
||||||
|
},
|
||||||
|
"expires-at": {
|
||||||
|
"label": "Data de expiração"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peer-multi-create": {
|
||||||
|
"headline-peer": "Criar múltiplos peers",
|
||||||
|
"headline-endpoint": "Criar múltiplos endpoints",
|
||||||
|
"identifiers": {
|
||||||
|
"label": "Identificadores de utilizador",
|
||||||
|
"placeholder": "Identificadores de utilizador",
|
||||||
|
"description": "Um identificador de utilizador (nome de utilizador) para o qual um peer deve ser criado."
|
||||||
|
},
|
||||||
|
"prefix": {
|
||||||
|
"headline-peer": "Peer:",
|
||||||
|
"headline-endpoint": "Endpoint:",
|
||||||
|
"label": "Prefixo do nome exibido",
|
||||||
|
"placeholder": "O prefixo",
|
||||||
|
"description": "Um prefixo que será adicionado ao nome exibido do peer."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"title": "Definições",
|
|
||||||
"password": {
|
|
||||||
"label": "Nova Palavra-passe",
|
|
||||||
"placeholder": "Deixe em branco para manter a atual"
|
|
||||||
},
|
|
||||||
"save": "Guardar Alterações"
|
|
||||||
},
|
|
||||||
"audit": {
|
|
||||||
"title": "Registo de Auditoria",
|
|
||||||
"username": "Utilizador",
|
|
||||||
"ip": "Endereço IP",
|
|
||||||
"method": "Método",
|
|
||||||
"path": "Caminho",
|
|
||||||
"status": "Estado",
|
|
||||||
"timestamp": "Data/Hora"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"required": "Este campo é obrigatório",
|
|
||||||
"invalid-email": "Endereço de email inválido",
|
|
||||||
"invalid-address": "Endereço inválido",
|
|
||||||
"invalid-endpoint": "Endpoint inválido",
|
|
||||||
"invalid-allowed-ips": "Formato de IPs Permitidos inválido"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,17 @@ import { defineStore } from 'pinia'
|
|||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||||
import router from '../router'
|
import router from '../router'
|
||||||
|
import { browserSupportsWebAuthn,startRegistration,startAuthentication } from '@simplewebauthn/browser';
|
||||||
|
import {base64_url_encode} from "@/helpers/encoding";
|
||||||
|
|
||||||
export const authStore = defineStore('auth',{
|
export const authStore = defineStore('auth',{
|
||||||
state: () => ({
|
state: () => ({
|
||||||
// initialize state from local storage to enable user to stay logged in
|
// initialize state from local storage to enable user to stay logged in
|
||||||
user: JSON.parse(localStorage.getItem('user')),
|
user: JSON.parse(localStorage.getItem('user')),
|
||||||
providers: [],
|
providers: [],
|
||||||
returnUrl: localStorage.getItem('returnUrl')
|
returnUrl: localStorage.getItem('returnUrl'),
|
||||||
|
webAuthnCredentials: [],
|
||||||
|
fetching: false,
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
UserIdentifier: (state) => state.user?.Identifier || 'unknown',
|
UserIdentifier: (state) => state.user?.Identifier || 'unknown',
|
||||||
@ -18,6 +22,14 @@ export const authStore = defineStore('auth',{
|
|||||||
IsAuthenticated: (state) => state.user != null,
|
IsAuthenticated: (state) => state.user != null,
|
||||||
IsAdmin: (state) => state.user?.IsAdmin || false,
|
IsAdmin: (state) => state.user?.IsAdmin || false,
|
||||||
ReturnUrl: (state) => state.returnUrl || '/',
|
ReturnUrl: (state) => state.returnUrl || '/',
|
||||||
|
IsWebAuthnEnabled: (state) => {
|
||||||
|
if (state.webAuthnCredentials) {
|
||||||
|
return state.webAuthnCredentials.length > 0
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
WebAuthnCredentials: (state) => state.webAuthnCredentials || [],
|
||||||
|
isFetching: (state) => state.fetching,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
SetReturnUrl(link) {
|
SetReturnUrl(link) {
|
||||||
@ -60,6 +72,23 @@ export const authStore = defineStore('auth',{
|
|||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
// LoadWebAuthnCredentials returns promise that might have been rejected if the session was not authenticated.
|
||||||
|
async LoadWebAuthnCredentials() {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.get(`/auth/webauthn/credentials`)
|
||||||
|
.then(credentials => {
|
||||||
|
this.setWebAuthnCredentials(credentials)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.setWebAuthnCredentials([])
|
||||||
|
console.log("Failed to load webauthn credentials:", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: error,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
// Login returns promise that might have been rejected if the login attempt was not successful.
|
// Login returns promise that might have been rejected if the login attempt was not successful.
|
||||||
async Login(username, password) {
|
async Login(username, password) {
|
||||||
return apiWrapper.post(`/auth/login`, { username, password })
|
return apiWrapper.post(`/auth/login`, { username, password })
|
||||||
@ -93,6 +122,157 @@ export const authStore = defineStore('auth',{
|
|||||||
|
|
||||||
await router.push('/login')
|
await router.push('/login')
|
||||||
},
|
},
|
||||||
|
async RegisterWebAuthn() {
|
||||||
|
// check if the browser supports WebAuthn
|
||||||
|
if (!browserSupportsWebAuthn()) {
|
||||||
|
console.error("WebAuthn is not supported by this browser.");
|
||||||
|
notify({
|
||||||
|
title: "WebAuthn not supported",
|
||||||
|
text: "This browser does not support WebAuthn.",
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return Promise.reject(new Error("WebAuthn not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetching = true
|
||||||
|
console.log("Starting WebAuthn registration...")
|
||||||
|
await apiWrapper.post(`/auth/webauthn/register/start`, {})
|
||||||
|
.then(optionsJSON => {
|
||||||
|
notify({
|
||||||
|
title: "Passkey registration",
|
||||||
|
text: "Starting passkey registration, follow the instructions in the browser."
|
||||||
|
});
|
||||||
|
console.log("Started WebAuthn registration with options: ", optionsJSON)
|
||||||
|
|
||||||
|
return startRegistration({ optionsJSON: optionsJSON.publicKey }).then(attResp => {
|
||||||
|
console.log("Finishing WebAuthn registration...")
|
||||||
|
return apiWrapper.post(`/auth/webauthn/register/finish`, attResp)
|
||||||
|
.then(credentials => {
|
||||||
|
console.log("Passkey registration finished successfully: ", credentials)
|
||||||
|
this.setWebAuthnCredentials(credentials)
|
||||||
|
notify({
|
||||||
|
title: "Passkey registration",
|
||||||
|
text: "A new passkey has been registered successfully!",
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.fetching = false
|
||||||
|
console.error("Failed to register passkey:", err);
|
||||||
|
notify({
|
||||||
|
title: "Passkey registration failed",
|
||||||
|
text: err,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
this.fetching = false
|
||||||
|
console.error("Failed to start WebAuthn registration:", err);
|
||||||
|
notify({
|
||||||
|
title: "Failed to start Passkey registration",
|
||||||
|
text: err,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.fetching = false
|
||||||
|
console.error("Failed to start WebAuthn registration:", err);
|
||||||
|
notify({
|
||||||
|
title: "Failed to start WebAuthn registration",
|
||||||
|
text: err,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async DeleteWebAuthnCredential(credentialId) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.delete(`/auth/webauthn/credential/${base64_url_encode(credentialId)}`)
|
||||||
|
.then(credentials => {
|
||||||
|
this.setWebAuthnCredentials(credentials)
|
||||||
|
notify({
|
||||||
|
title: "Success",
|
||||||
|
text: "Passkey deleted successfully!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.fetching = false
|
||||||
|
console.error("Failed to delete webauthn credential:", err);
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async RenameWebAuthnCredential(credential) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.put(`/auth/webauthn/credential/${base64_url_encode(credential.ID)}`, {
|
||||||
|
Name: credential.Name,
|
||||||
|
})
|
||||||
|
.then(credentials => {
|
||||||
|
this.setWebAuthnCredentials(credentials)
|
||||||
|
notify({
|
||||||
|
title: "Success",
|
||||||
|
text: "Passkey renamed successfully!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.fetching = false
|
||||||
|
console.error("Failed to rename webauthn credential", credential.ID, ":", err);
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async LoginWebAuthn() {
|
||||||
|
// check if the browser supports WebAuthn
|
||||||
|
if (!browserSupportsWebAuthn()) {
|
||||||
|
console.error("WebAuthn is not supported by this browser.");
|
||||||
|
notify({
|
||||||
|
title: "WebAuthn not supported",
|
||||||
|
text: "This browser does not support WebAuthn.",
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return Promise.reject(new Error("WebAuthn not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetching = true
|
||||||
|
console.log("Starting WebAuthn login...")
|
||||||
|
await apiWrapper.post(`/auth/webauthn/login/start`, {})
|
||||||
|
.then(optionsJSON => {
|
||||||
|
console.log("Started WebAuthn login with options: ", optionsJSON)
|
||||||
|
|
||||||
|
return startAuthentication({ optionsJSON: optionsJSON.publicKey }).then(asseResp => {
|
||||||
|
console.log("Finishing WebAuthn login ...")
|
||||||
|
return apiWrapper.post(`/auth/webauthn/login/finish`, asseResp)
|
||||||
|
.then(user => {
|
||||||
|
console.log("Passkey login finished successfully for user:", user.Identifier)
|
||||||
|
this.ResetReturnUrl()
|
||||||
|
this.setUserInfo(user)
|
||||||
|
return user.Identifier
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Failed to login with passkey:", err)
|
||||||
|
this.setUserInfo(null)
|
||||||
|
return Promise.reject(new Error("login failed"))
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Failed to finish passkey login:", err)
|
||||||
|
this.setUserInfo(null)
|
||||||
|
return Promise.reject(new Error("login failed"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Failed to start passkey login:", err)
|
||||||
|
this.setUserInfo(null)
|
||||||
|
return Promise.reject(new Error("login failed"))
|
||||||
|
})
|
||||||
|
},
|
||||||
// -- internal setters
|
// -- internal setters
|
||||||
setUserInfo(userInfo) {
|
setUserInfo(userInfo) {
|
||||||
// store user details and jwt in local storage to keep user logged in between page refreshes
|
// store user details and jwt in local storage to keep user logged in between page refreshes
|
||||||
@ -120,5 +300,9 @@ export const authStore = defineStore('auth',{
|
|||||||
localStorage.removeItem('user')
|
localStorage.removeItem('user')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setWebAuthnCredentials(credentials) {
|
||||||
|
this.fetching = false
|
||||||
|
this.webAuthnCredentials = credentials
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -129,7 +129,7 @@ export const profileStore = defineStore('profile', {
|
|||||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
|
||||||
.then(this.setUser)
|
.then(this.setUser)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.setPeers([])
|
this.fetching = false
|
||||||
console.log("Failed to activate API for ", currentUser, ": ", error)
|
console.log("Failed to activate API for ", currentUser, ": ", error)
|
||||||
notify({
|
notify({
|
||||||
title: "Backend Connection Failure",
|
title: "Backend Connection Failure",
|
||||||
@ -143,7 +143,7 @@ export const profileStore = defineStore('profile', {
|
|||||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
|
||||||
.then(this.setUser)
|
.then(this.setUser)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.setPeers([])
|
this.fetching = false
|
||||||
console.log("Failed to deactivate API for ", currentUser, ": ", error)
|
console.log("Failed to deactivate API for ", currentUser, ": ", error)
|
||||||
notify({
|
notify({
|
||||||
title: "Backend Connection Failure",
|
title: "Backend Connection Failure",
|
||||||
|
@ -65,7 +65,7 @@ const auth = authStore()
|
|||||||
<div class="card-body d-flex flex-column">
|
<div class="card-body d-flex flex-column">
|
||||||
<h4 class="card-title">{{ $t('home.about-portal.headline') }}</h4>
|
<h4 class="card-title">{{ $t('home.about-portal.headline') }}</h4>
|
||||||
<p class="card-text">{{ $t('home.about-portal.content') }}</p>
|
<p class="card-text">{{ $t('home.about-portal.content') }}</p>
|
||||||
<a href="https://github.com/h44z/wg-portal/" title="WireGuard Portal" target="_blank"
|
<a href="https://wgportal.org/" title="WireGuard Portal" target="_blank"
|
||||||
rel="noopener noreferrer" class="mt-auto btn btn-primary btn-sm">{{ $t('home.about-portal.button') }}</a>
|
rel="noopener noreferrer" class="mt-auto btn btn-primary btn-sm">{{ $t('home.about-portal.button') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import {computed, ref} from "vue";
|
import {computed, onMounted, ref} from "vue";
|
||||||
import {authStore} from "@/stores/auth";
|
import {authStore} from "@/stores/auth";
|
||||||
import router from '../router/index.js'
|
import router from '../router/index.js'
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
@ -16,6 +16,14 @@ const password = ref("")
|
|||||||
const usernameInvalid = computed(() => username.value === "")
|
const usernameInvalid = computed(() => username.value === "")
|
||||||
const passwordInvalid = computed(() => password.value === "")
|
const passwordInvalid = computed(() => password.value === "")
|
||||||
const disableLoginBtn = computed(() => username.value === "" || password.value === "" || loggingIn.value)
|
const disableLoginBtn = computed(() => username.value === "" || password.value === "" || loggingIn.value)
|
||||||
|
const showLoginForm = computed(() => {
|
||||||
|
console.log(router.currentRoute.value.query)
|
||||||
|
return settings.Setting('LoginFormVisible') || router.currentRoute.value.query.hasOwnProperty('all');
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await settings.LoadSettings()
|
||||||
|
})
|
||||||
|
|
||||||
const login = async function () {
|
const login = async function () {
|
||||||
console.log("Performing login for user:", username.value);
|
console.log("Performing login for user:", username.value);
|
||||||
@ -28,7 +36,34 @@ const login = async function () {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
loggingIn.value = false;
|
loggingIn.value = false;
|
||||||
settings.LoadSettings(); // only logs errors, does not throw
|
settings.LoadSettings(); // reload full settings
|
||||||
|
router.push(auth.ReturnUrl);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
notify({
|
||||||
|
title: "Login failed!",
|
||||||
|
text: "Authentication failed!",
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
|
//loggingIn.value = false;
|
||||||
|
// delay the user from logging in for a short amount of time
|
||||||
|
setTimeout(() => loggingIn.value = false, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginWebAuthn = async function () {
|
||||||
|
console.log("Performing webauthn login");
|
||||||
|
loggingIn.value = true;
|
||||||
|
auth.LoginWebAuthn()
|
||||||
|
.then(uid => {
|
||||||
|
notify({
|
||||||
|
title: "Logged in",
|
||||||
|
text: "Authentication succeeded!",
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
loggingIn.value = false;
|
||||||
|
settings.LoadSettings(); // reload full settings
|
||||||
router.push(auth.ReturnUrl);
|
router.push(auth.ReturnUrl);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@ -66,7 +101,7 @@ const externalLogin = function (provider) {
|
|||||||
</div></div>
|
</div></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<fieldset>
|
<fieldset v-if="showLoginForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="inputUsername">{{ $t('login.username.label') }}</label>
|
<label class="form-label" for="inputUsername">{{ $t('login.username.label') }}</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
@ -85,17 +120,46 @@ const externalLogin = function (provider) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-5 d-flex">
|
<div class="row mt-5 mb-2">
|
||||||
<div :class="{'col-lg-4':auth.LoginProviders.length < 3, 'col-lg-12':auth.LoginProviders.length >= 3}" class="d-flex mb-2">
|
<div class="col-sm-4 col-xs-12">
|
||||||
<button :disabled="disableLoginBtn" class="btn btn-primary flex-fill" type="submit" @click.prevent="login">
|
<button :disabled="disableLoginBtn" class="btn btn-primary mb-2" type="submit" @click.prevent="login">
|
||||||
{{ $t('login.button') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
{{ $t('login.button') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="{'col-lg-8':auth.LoginProviders.length < 3, 'col-lg-12':auth.LoginProviders.length >= 3}" class="d-flex mb-2">
|
<div class="col-sm-8 col-xs-12 text-sm-end">
|
||||||
|
<button v-if="settings.Setting('WebAuthnEnabled')" class="btn btn-primary" type="submit" @click.prevent="loginWebAuthn">
|
||||||
|
{{ $t('login.button-webauthn') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4 d-flex">
|
||||||
|
<div class="col-lg-12 d-flex mb-2">
|
||||||
<!-- OpenIdConnect / OAUTH providers -->
|
<!-- OpenIdConnect / OAUTH providers -->
|
||||||
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
||||||
:disabled="loggingIn" :title="provider.Name" class="btn btn-outline-primary flex-fill"
|
:disabled="loggingIn" :title="provider.Name" class="btn btn-outline-primary flex-fill"
|
||||||
v-html="provider.Name" @click.prevent="externalLogin(provider)"></button>
|
v-html="provider.Name" @click.prevent="externalLogin(provider)"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset v-else>
|
||||||
|
<div class="row mt-1 mb-2" v-if="settings.Setting('WebAuthnEnabled')">
|
||||||
|
<div class="col-lg-12 d-flex mb-2">
|
||||||
|
<button class="btn btn-outline-primary flex-fill" type="submit" @click.prevent="loginWebAuthn">
|
||||||
|
{{ $t('login.button-webauthn') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-1 d-flex">
|
||||||
|
<div class="col-lg-12 d-flex mb-2">
|
||||||
|
<!-- OpenIdConnect / OAUTH providers -->
|
||||||
|
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
||||||
|
:disabled="loggingIn" :title="provider.Name" class="btn btn-outline-primary flex-fill"
|
||||||
|
v-html="provider.Name" @click.prevent="externalLogin(provider)"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -104,7 +168,6 @@ const externalLogin = function (provider) {
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
import { profileStore } from "@/stores/profile";
|
import { profileStore } from "@/stores/profile";
|
||||||
import { settingsStore } from "@/stores/settings";
|
import { settingsStore } from "@/stores/settings";
|
||||||
import { authStore } from "../stores/auth";
|
import { authStore } from "../stores/auth";
|
||||||
@ -10,8 +10,30 @@ const auth = authStore()
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await profile.LoadUser()
|
await profile.LoadUser()
|
||||||
|
await auth.LoadWebAuthnCredentials()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedCredential = ref({})
|
||||||
|
|
||||||
|
function enableRename(credential) {
|
||||||
|
credential.renameMode = true;
|
||||||
|
credential.tempName = credential.Name; // Store the original name
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelRename(credential) {
|
||||||
|
credential.renameMode = false;
|
||||||
|
credential.tempName = null; // Discard changes
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRename(credential) {
|
||||||
|
try {
|
||||||
|
await auth.RenameWebAuthnCredential({ ...credential, Name: credential.tempName });
|
||||||
|
credential.Name = credential.tempName; // Update the name
|
||||||
|
credential.renameMode = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to rename credential:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -69,4 +91,86 @@ onMounted(async () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-light p-5 mt-5" v-if="settings.Setting('WebAuthnEnabled')">
|
||||||
|
<h2 class="display-7">{{ $t('settings.webauthn.headline') }}</h2>
|
||||||
|
<p class="lead">{{ $t('settings.webauthn.abstract') }}</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
<p v-if="auth.IsWebAuthnEnabled">{{ $t('settings.webauthn.active-description') }}</p>
|
||||||
|
<p v-else>{{ $t('settings.webauthn.inactive-description') }}</p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<button class="input-group-text btn btn-primary" :title="$t('settings.webauthn.button-register-text')" @click.prevent="auth.RegisterWebAuthn" :disabled="auth.isFetching">
|
||||||
|
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.webauthn.button-register-title') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="auth.WebAuthnCredentials.length > 0" class="mt-4">
|
||||||
|
<h3>{{ $t('settings.webauthn.credentials-list') }}</h3>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 50%">{{ $t('settings.webauthn.table.name') }}</th>
|
||||||
|
<th style="width: 20%">{{ $t('settings.webauthn.table.created') }}</th>
|
||||||
|
<th style="width: 30%">{{ $t('settings.webauthn.table.actions') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="credential in auth.webAuthnCredentials" :key="credential.ID">
|
||||||
|
<td class="align-middle">
|
||||||
|
<div v-if="credential.renameMode">
|
||||||
|
<input v-model="credential.tempName" class="form-control" type="text" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{ credential.Name }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
{{ credential.CreatedAt }}
|
||||||
|
</td>
|
||||||
|
<td class="align-middle text-center">
|
||||||
|
<div v-if="credential.renameMode">
|
||||||
|
<button class="btn btn-success me-1" :title="$t('settings.webauthn.button-save-text')" @click.prevent="saveRename(credential)" :disabled="auth.isFetching">
|
||||||
|
{{ $t('settings.webauthn.button-save-title') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" :title="$t('settings.webauthn.button-cancel-text')" @click.prevent="cancelRename(credential)">
|
||||||
|
{{ $t('settings.webauthn.button-cancel-title') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<button class="btn btn-secondary me-1" :title="$t('settings.webauthn.button-rename-text')" @click.prevent="enableRename(credential)">
|
||||||
|
{{ $t('settings.webauthn.button-rename-title') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" :title="$t('settings.webauthn.button-delete-text')" data-bs-toggle="modal" data-bs-target="#webAuthnDeleteModal" :disabled="auth.isFetching" @click="selectedCredential=credential">
|
||||||
|
{{ $t('settings.webauthn.button-delete-title') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="webAuthnDeleteModal" tabindex="-1" aria-labelledby="webAuthnDeleteModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger text-white">
|
||||||
|
<h5 class="modal-title" id="webAuthnDeleteModalLabel">{{ $t('settings.webauthn.modal-delete.headline') }}</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" :aria-label="$t('settings.webauthn.modal-delete.button-cancel')"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h5 class="mb-3">{{ selectedCredential.Name }} <small class="text-body-secondary">({{ $t('settings.webauthn.modal-delete.created') }} {{ selectedCredential.CreatedAt }})</small></h5>
|
||||||
|
<p class="mb-0">{{ $t('settings.webauthn.modal-delete.abstract') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t('settings.webauthn.modal-delete.button-cancel') }}</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="confirmWebAuthnDelete" @click="auth.DeleteWebAuthnCredential(selectedCredential.ID)" :disabled="auth.isFetching" data-bs-dismiss="modal">{{ $t('settings.webauthn.modal-delete.button-delete') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
32
go.mod
32
go.mod
@ -10,25 +10,26 @@ require (
|
|||||||
github.com/go-ldap/ldap/v3 v3.4.11
|
github.com/go-ldap/ldap/v3 v3.4.11
|
||||||
github.com/go-pkgz/routegroup v1.4.1
|
github.com/go-pkgz/routegroup v1.4.1
|
||||||
github.com/go-playground/validator/v10 v10.26.0
|
github.com/go-playground/validator/v10 v10.26.0
|
||||||
|
github.com/go-webauthn/webauthn v0.13.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/prometheus-community/pro-bing v0.7.0
|
github.com/prometheus-community/pro-bing v0.7.0
|
||||||
github.com/prometheus/client_golang v1.22.0
|
github.com/prometheus/client_golang v1.22.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/swaggo/swag v1.16.4
|
github.com/swaggo/swag v1.16.4
|
||||||
github.com/vardius/message-bus v1.1.5
|
github.com/vardius/message-bus v1.1.5
|
||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.1
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||||
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
|
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
|
||||||
golang.org/x/crypto v0.37.0
|
golang.org/x/crypto v0.39.0
|
||||||
golang.org/x/oauth2 v0.29.0
|
golang.org/x/oauth2 v0.30.0
|
||||||
golang.org/x/sys v0.32.0
|
golang.org/x/sys v0.33.0
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/mysql v1.5.7
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/driver/postgres v1.5.11
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/driver/sqlserver v1.5.4
|
gorm.io/driver/sqlserver v1.6.0
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -39,6 +40,7 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
@ -51,9 +53,12 @@ require (
|
|||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.2 // indirect
|
github.com/go-sql-driver/mysql v1.9.2 // indirect
|
||||||
github.com/go-test/deep v1.1.1 // indirect
|
github.com/go-test/deep v1.1.1 // indirect
|
||||||
|
github.com/go-webauthn/x v0.1.21 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/google/go-tpm v0.9.5 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.7.4 // indirect
|
github.com/jackc/pgx/v5 v5.7.4 // indirect
|
||||||
@ -62,7 +67,6 @@ require (
|
|||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.9.0 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@ -70,6 +74,7 @@ require (
|
|||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/microsoft/go-mssqldb v1.8.0 // indirect
|
github.com/microsoft/go-mssqldb v1.8.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
@ -79,12 +84,13 @@ require (
|
|||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
golang.org/x/net v0.39.0 // indirect
|
golang.org/x/net v0.40.0 // indirect
|
||||||
golang.org/x/sync v0.13.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
golang.org/x/tools v0.32.0 // indirect
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
modernc.org/libc v1.63.0 // indirect
|
modernc.org/libc v1.63.0 // indirect
|
||||||
|
205
go.sum
205
go.sum
@ -1,16 +1,13 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
|
||||||
@ -19,8 +16,7 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occ
|
|||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
@ -44,24 +40,18 @@ github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/
|
|||||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
|
||||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
||||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||||
@ -82,17 +72,20 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
|
||||||
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
|
|
||||||
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
|
||||||
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/go-webauthn/webauthn v0.13.0 h1:cJIL1/1l+22UekVhipziAaSgESJxokYkowUqAIsWs0Y=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/go-webauthn/webauthn v0.13.0/go.mod h1:Oy9o2o79dbLKRPZWWgRIOdtBGAhKnDIaBp2PFkICRHs=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/go-webauthn/x v0.1.21 h1:nFbckQxudvHEJn2uy1VEi713MeSpApoAv9eRqsb9AdQ=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/go-webauthn/x v0.1.21/go.mod h1:sEYohtg1zL4An1TXIUIQ5csdmoO+WO0R4R2pGKaHYKA=
|
||||||
|
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
@ -101,11 +94,12 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
|||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
@ -117,8 +111,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.7.3 h1:PO1wNKj/bTAwxSJnO1Z4Ai8j4magtqg2SLNjEDzcXQo=
|
|
||||||
github.com/jackc/pgx/v5 v5.7.3/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
|
||||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
@ -127,10 +119,12 @@ github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFK
|
|||||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||||
|
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||||
|
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||||
@ -163,32 +157,29 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
|
|||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
|
||||||
github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
|
github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
|
||||||
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
|
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||||
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus-community/pro-bing v0.6.1 h1:EQukUOma9YFZRPe4DGSscxUf9LH07rpqwisNWjSZrgU=
|
|
||||||
github.com/prometheus-community/pro-bing v0.6.1/go.mod h1:jNCOI3D7pmTCeaoF41cNS6uaxeFY/Gmc3ffwbuJVzAQ=
|
|
||||||
github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
|
github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
|
||||||
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
|
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
|
||||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
|
||||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
|
||||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||||
@ -200,15 +191,10 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
|
|||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||||
@ -218,11 +204,12 @@ github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5
|
|||||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
|
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
|
||||||
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
|
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
|
||||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
|
||||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||||
@ -233,22 +220,16 @@ github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr
|
|||||||
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
|
||||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
@ -256,157 +237,129 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
|
||||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
|
||||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
|
||||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
|
||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
|
||||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
|
||||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
||||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
|
||||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
|
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
|
||||||
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
|
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
|
||||||
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
|
||||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
|
||||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
|
||||||
modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
|
modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
|
||||||
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
|
modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
|
|
||||||
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
|
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
|
||||||
|
modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY=
|
||||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
|
|
||||||
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
|
||||||
modernc.org/libc v1.63.0 h1:wKzb61wOGCzgahQBORb1b0dZonh8Ufzl/7r4Yf1D5YA=
|
modernc.org/libc v1.63.0 h1:wKzb61wOGCzgahQBORb1b0dZonh8Ufzl/7r4Yf1D5YA=
|
||||||
modernc.org/libc v1.63.0/go.mod h1:wDzH1mgz1wUIEwottFt++POjGRO9sgyQKrpXaz3x89E=
|
modernc.org/libc v1.63.0/go.mod h1:wDzH1mgz1wUIEwottFt++POjGRO9sgyQKrpXaz3x89E=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
|
|
||||||
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
|
||||||
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
||||||
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=
|
|
||||||
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
|
|
||||||
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
@ -220,6 +220,8 @@ func (r *SqlRepo) preCheck() error {
|
|||||||
func (r *SqlRepo) migrate() error {
|
func (r *SqlRepo) migrate() error {
|
||||||
slog.Debug("running migration: sys-stat", "result", r.db.AutoMigrate(&SysStat{}))
|
slog.Debug("running migration: sys-stat", "result", r.db.AutoMigrate(&SysStat{}))
|
||||||
slog.Debug("running migration: user", "result", r.db.AutoMigrate(&domain.User{}))
|
slog.Debug("running migration: user", "result", r.db.AutoMigrate(&domain.User{}))
|
||||||
|
slog.Debug("running migration: user webauthn credentials", "result",
|
||||||
|
r.db.AutoMigrate(&domain.UserWebauthnCredential{}))
|
||||||
slog.Debug("running migration: interface", "result", r.db.AutoMigrate(&domain.Interface{}))
|
slog.Debug("running migration: interface", "result", r.db.AutoMigrate(&domain.Interface{}))
|
||||||
slog.Debug("running migration: peer", "result", r.db.AutoMigrate(&domain.Peer{}))
|
slog.Debug("running migration: peer", "result", r.db.AutoMigrate(&domain.Peer{}))
|
||||||
slog.Debug("running migration: peer status", "result", r.db.AutoMigrate(&domain.PeerStatus{}))
|
slog.Debug("running migration: peer status", "result", r.db.AutoMigrate(&domain.PeerStatus{}))
|
||||||
@ -746,7 +748,7 @@ func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr
|
|||||||
func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||||
var user domain.User
|
var user domain.User
|
||||||
|
|
||||||
err := r.db.WithContext(ctx).First(&user, id).Error
|
err := r.db.WithContext(ctx).Preload("WebAuthnCredentialList").First(&user, id).Error
|
||||||
|
|
||||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, domain.ErrNotFound
|
return nil, domain.ErrNotFound
|
||||||
@ -764,7 +766,7 @@ func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domai
|
|||||||
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
|
|
||||||
err := r.db.WithContext(ctx).Where("email = ?", email).Find(&users).Error
|
err := r.db.WithContext(ctx).Where("email = ?", email).Preload("WebAuthnCredentialList").Find(&users).Error
|
||||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, domain.ErrNotFound
|
return nil, domain.ErrNotFound
|
||||||
}
|
}
|
||||||
@ -785,11 +787,26 @@ func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.Use
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserByWebAuthnCredential returns the user with the given webauthn credential id.
|
||||||
|
func (r *SqlRepo) GetUserByWebAuthnCredential(ctx context.Context, credentialIdBase64 string) (*domain.User, error) {
|
||||||
|
var credential domain.UserWebauthnCredential
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Where("credential_identifier = ?", credentialIdBase64).First(&credential).Error
|
||||||
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.GetUser(ctx, domain.UserIdentifier(credential.UserIdentifier))
|
||||||
|
}
|
||||||
|
|
||||||
// GetAllUsers returns all users.
|
// GetAllUsers returns all users.
|
||||||
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
|
|
||||||
err := r.db.WithContext(ctx).Find(&users).Error
|
err := r.db.WithContext(ctx).Preload("WebAuthnCredentialList").Find(&users).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -808,6 +825,7 @@ func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User,
|
|||||||
Or("firstname LIKE ?", searchValue).
|
Or("firstname LIKE ?", searchValue).
|
||||||
Or("lastname LIKE ?", searchValue).
|
Or("lastname LIKE ?", searchValue).
|
||||||
Or("email LIKE ?", searchValue).
|
Or("email LIKE ?", searchValue).
|
||||||
|
Preload("WebAuthnCredentialList").
|
||||||
Find(&users).Error
|
Find(&users).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -853,7 +871,7 @@ func (r *SqlRepo) SaveUser(
|
|||||||
|
|
||||||
// DeleteUser deletes the user with the given id.
|
// DeleteUser deletes the user with the given id.
|
||||||
func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) error {
|
func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) error {
|
||||||
err := r.db.WithContext(ctx).Delete(&domain.User{}, id).Error
|
err := r.db.WithContext(ctx).Unscoped().Select(clause.Associations).Delete(&domain.User{Identifier: id}).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -897,6 +915,11 @@ func (r *SqlRepo) upsertUser(ui *domain.ContextUserInfo, tx *gorm.DB, user *doma
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = tx.Session(&gorm.Session{FullSaveAssociations: true}).Unscoped().Model(user).Association("WebAuthnCredentialList").Unscoped().Replace(user.WebAuthnCredentialList)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update users webauthn credentials: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,5 +133,5 @@ func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerS
|
|||||||
}
|
}
|
||||||
m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived))
|
m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived))
|
||||||
m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted))
|
m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted))
|
||||||
m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected()))
|
m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected))
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/auth/login/{provider}/callback": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Handle the OAuth callback.",
|
||||||
|
"operationId": "auth_handleOauthCallbackGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/login/{provider}/init": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Initiate the OAuth login flow.",
|
||||||
|
"operationId": "auth_handleOauthInitiateGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/logout": {
|
"/auth/logout": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@ -129,30 +175,78 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/{provider}/callback": {
|
"/auth/webauthn/credential/{id}": {
|
||||||
"get": {
|
"put": {
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"Authentication"
|
"Authentication"
|
||||||
],
|
],
|
||||||
"summary": "Handle the OAuth callback.",
|
"summary": "Update a WebAuthn credential.",
|
||||||
"operationId": "auth_handleOauthCallbackGet",
|
"operationId": "auth_handleWebAuthnCredentialsPut",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Base64 encoded Credential ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Credential name",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.WebAuthnCredentialRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Delete a WebAuthn credential.",
|
||||||
|
"operationId": "auth_handleWebAuthnCredentialsDelete",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Base64 encoded Credential ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/{provider}/init": {
|
"/auth/webauthn/credentials": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@ -160,15 +254,67 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Authentication"
|
"Authentication"
|
||||||
],
|
],
|
||||||
"summary": "Initiate the OAuth login flow.",
|
"summary": "Get all available external login providers.",
|
||||||
"operationId": "auth_handleOauthInitiateGet",
|
"operationId": "auth_handleWebAuthnCredentialsGet",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/webauthn/login/finish": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Finish the WebAuthn login process.",
|
||||||
|
"operationId": "auth_handleWebAuthnLoginFinish",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.User"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/webauthn/register/finish": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Finish the WebAuthn registration process.",
|
||||||
|
"operationId": "auth_handleWebAuthnRegisterFinish",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"default": "\"\"",
|
||||||
|
"description": "Credential name",
|
||||||
|
"name": "credential_name",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2085,14 +2231,23 @@
|
|||||||
"ApiAdminOnly": {
|
"ApiAdminOnly": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"LoginFormVisible": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"MailLinkOnly": {
|
"MailLinkOnly": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"MinPasswordLength": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"PersistentConfigSupported": {
|
"PersistentConfigSupported": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"SelfProvisioning": {
|
"SelfProvisioning": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"WebAuthnEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2161,6 +2316,28 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"model.WebAuthnCredentialRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.WebAuthnCredentialResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"CreatedAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -381,12 +381,18 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
ApiAdminOnly:
|
ApiAdminOnly:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
LoginFormVisible:
|
||||||
|
type: boolean
|
||||||
MailLinkOnly:
|
MailLinkOnly:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
MinPasswordLength:
|
||||||
|
type: integer
|
||||||
PersistentConfigSupported:
|
PersistentConfigSupported:
|
||||||
type: boolean
|
type: boolean
|
||||||
SelfProvisioning:
|
SelfProvisioning:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
WebAuthnEnabled:
|
||||||
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
model.User:
|
model.User:
|
||||||
properties:
|
properties:
|
||||||
@ -433,6 +439,20 @@ definitions:
|
|||||||
Source:
|
Source:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
model.WebAuthnCredentialRequest:
|
||||||
|
properties:
|
||||||
|
Name:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
model.WebAuthnCredentialResponse:
|
||||||
|
properties:
|
||||||
|
CreatedAt:
|
||||||
|
type: string
|
||||||
|
ID:
|
||||||
|
type: string
|
||||||
|
Name:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
info:
|
info:
|
||||||
contact:
|
contact:
|
||||||
name: WireGuard Portal Developers
|
name: WireGuard Portal Developers
|
||||||
@ -456,7 +476,22 @@ paths:
|
|||||||
summary: Get all available audit entries. Ordered by timestamp.
|
summary: Get all available audit entries. Ordered by timestamp.
|
||||||
tags:
|
tags:
|
||||||
- Audit
|
- Audit
|
||||||
/auth/{provider}/callback:
|
/auth/login:
|
||||||
|
post:
|
||||||
|
operationId: auth_handleLoginPost
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.LoginProviderInfo'
|
||||||
|
type: array
|
||||||
|
summary: Get all available external login providers.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
/auth/login/{provider}/callback:
|
||||||
get:
|
get:
|
||||||
operationId: auth_handleOauthCallbackGet
|
operationId: auth_handleOauthCallbackGet
|
||||||
produces:
|
produces:
|
||||||
@ -471,7 +506,7 @@ paths:
|
|||||||
summary: Handle the OAuth callback.
|
summary: Handle the OAuth callback.
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
/auth/{provider}/init:
|
/auth/login/{provider}/init:
|
||||||
get:
|
get:
|
||||||
operationId: auth_handleOauthInitiateGet
|
operationId: auth_handleOauthInitiateGet
|
||||||
produces:
|
produces:
|
||||||
@ -486,21 +521,6 @@ paths:
|
|||||||
summary: Initiate the OAuth login flow.
|
summary: Initiate the OAuth login flow.
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
/auth/login:
|
|
||||||
post:
|
|
||||||
operationId: auth_handleLoginPost
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/model.LoginProviderInfo'
|
|
||||||
type: array
|
|
||||||
summary: Get all available external login providers.
|
|
||||||
tags:
|
|
||||||
- Authentication
|
|
||||||
/auth/logout:
|
/auth/logout:
|
||||||
post:
|
post:
|
||||||
operationId: auth_handleLogoutPost
|
operationId: auth_handleLogoutPost
|
||||||
@ -548,6 +568,102 @@ paths:
|
|||||||
summary: Get information about the currently logged-in user.
|
summary: Get information about the currently logged-in user.
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
|
/auth/webauthn/credential/{id}:
|
||||||
|
delete:
|
||||||
|
operationId: auth_handleWebAuthnCredentialsDelete
|
||||||
|
parameters:
|
||||||
|
- description: Base64 encoded Credential ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||||
|
type: array
|
||||||
|
summary: Delete a WebAuthn credential.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
put:
|
||||||
|
operationId: auth_handleWebAuthnCredentialsPut
|
||||||
|
parameters:
|
||||||
|
- description: Base64 encoded Credential ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Credential name
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.WebAuthnCredentialRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||||
|
type: array
|
||||||
|
summary: Update a WebAuthn credential.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
/auth/webauthn/credentials:
|
||||||
|
get:
|
||||||
|
operationId: auth_handleWebAuthnCredentialsGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||||
|
type: array
|
||||||
|
summary: Get all available external login providers.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
/auth/webauthn/login/finish:
|
||||||
|
post:
|
||||||
|
operationId: auth_handleWebAuthnLoginFinish
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.User'
|
||||||
|
summary: Finish the WebAuthn login process.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
/auth/webauthn/register/finish:
|
||||||
|
post:
|
||||||
|
operationId: auth_handleWebAuthnRegisterFinish
|
||||||
|
parameters:
|
||||||
|
- default: '""'
|
||||||
|
description: Credential name
|
||||||
|
in: query
|
||||||
|
name: credential_name
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||||
|
type: array
|
||||||
|
summary: Finish the WebAuthn registration process.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
/config/frontend.js:
|
/config/frontend.js:
|
||||||
get:
|
get:
|
||||||
operationId: config_handleConfigJsGet
|
operationId: config_handleConfigJsGet
|
||||||
|
@ -118,6 +118,7 @@
|
|||||||
"BasicAuth": []
|
"BasicAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"description": "This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -250,6 +251,7 @@
|
|||||||
"BasicAuth": []
|
"BasicAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"description": "This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -309,6 +311,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/interface/prepare": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BasicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Interfaces"
|
||||||
|
],
|
||||||
|
"summary": "Prepare a new interface record.",
|
||||||
|
"operationId": "interfaces_handlePrepareGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Interface"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/metrics/by-interface/{id}": {
|
"/metrics/by-interface/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@ -547,7 +593,7 @@
|
|||||||
"BasicAuth": []
|
"BasicAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Only admins can update existing records.",
|
"description": "Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -779,7 +825,7 @@
|
|||||||
"BasicAuth": []
|
"BasicAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Only admins can create new records.",
|
"description": "Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -839,6 +885,71 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/peer/prepare/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BasicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Peers"
|
||||||
|
],
|
||||||
|
"summary": "Prepare a new peer record for the given WireGuard interface.",
|
||||||
|
"operationId": "peers_handlePrepareGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The interface identifier.",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Peer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/provisioning/data/peer-config": {
|
"/provisioning/data/peer-config": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -748,6 +748,8 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Interfaces
|
- Interfaces
|
||||||
put:
|
put:
|
||||||
|
description: This endpoint updates an existing interface with the provided data.
|
||||||
|
All required fields must be filled (e.g. name, private key, public key, ...).
|
||||||
operationId: interfaces_handleUpdatePut
|
operationId: interfaces_handleUpdatePut
|
||||||
parameters:
|
parameters:
|
||||||
- description: The interface identifier.
|
- description: The interface identifier.
|
||||||
@ -795,6 +797,8 @@ paths:
|
|||||||
- Interfaces
|
- Interfaces
|
||||||
/interface/new:
|
/interface/new:
|
||||||
post:
|
post:
|
||||||
|
description: This endpoint creates a new interface with the provided data. All
|
||||||
|
required fields must be filled (e.g. name, private key, public key, ...).
|
||||||
operationId: interfaces_handleCreatePost
|
operationId: interfaces_handleCreatePost
|
||||||
parameters:
|
parameters:
|
||||||
- description: The interface data.
|
- description: The interface data.
|
||||||
@ -835,6 +839,35 @@ paths:
|
|||||||
summary: Create a new interface record.
|
summary: Create a new interface record.
|
||||||
tags:
|
tags:
|
||||||
- Interfaces
|
- Interfaces
|
||||||
|
/interface/prepare:
|
||||||
|
get:
|
||||||
|
description: This endpoint returns a new interface with default values (fresh
|
||||||
|
key pair, valid name, new IP address pool, ...).
|
||||||
|
operationId: interfaces_handlePrepareGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Interface'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
security:
|
||||||
|
- BasicAuth: []
|
||||||
|
summary: Prepare a new interface record.
|
||||||
|
tags:
|
||||||
|
- Interfaces
|
||||||
/metrics/by-interface/{id}:
|
/metrics/by-interface/{id}:
|
||||||
get:
|
get:
|
||||||
operationId: metrics_handleMetricsForInterfaceGet
|
operationId: metrics_handleMetricsForInterfaceGet
|
||||||
@ -1024,7 +1057,8 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Peers
|
- Peers
|
||||||
put:
|
put:
|
||||||
description: Only admins can update existing records.
|
description: Only admins can update existing records. The peer record must contain
|
||||||
|
all required fields (e.g., public key, allowed IPs).
|
||||||
operationId: peers_handleUpdatePut
|
operationId: peers_handleUpdatePut
|
||||||
parameters:
|
parameters:
|
||||||
- description: The peer identifier.
|
- description: The peer identifier.
|
||||||
@ -1136,7 +1170,8 @@ paths:
|
|||||||
- Peers
|
- Peers
|
||||||
/peer/new:
|
/peer/new:
|
||||||
post:
|
post:
|
||||||
description: Only admins can create new records.
|
description: Only admins can create new records. The peer record must contain
|
||||||
|
all required fields (e.g., public key, allowed IPs).
|
||||||
operationId: peers_handleCreatePost
|
operationId: peers_handleCreatePost
|
||||||
parameters:
|
parameters:
|
||||||
- description: The peer data.
|
- description: The peer data.
|
||||||
@ -1177,6 +1212,49 @@ paths:
|
|||||||
summary: Create a new peer record.
|
summary: Create a new peer record.
|
||||||
tags:
|
tags:
|
||||||
- Peers
|
- Peers
|
||||||
|
/peer/prepare/{id}:
|
||||||
|
get:
|
||||||
|
description: This endpoint is used to prepare a new peer record. The returned
|
||||||
|
data contains a fresh key pair and valid ip address.
|
||||||
|
operationId: peers_handlePrepareGet
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Peer'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
security:
|
||||||
|
- BasicAuth: []
|
||||||
|
summary: Prepare a new peer record for the given WireGuard interface.
|
||||||
|
tags:
|
||||||
|
- Peers
|
||||||
/provisioning/data/peer-config:
|
/provisioning/data/peer-config:
|
||||||
get:
|
get:
|
||||||
description: Normal users can only access their own record. Admins can access
|
description: Normal users can only access their own record. Admins can access
|
||||||
|
@ -99,6 +99,8 @@ type Authenticator interface {
|
|||||||
LoggedIn(scopes ...Scope) func(next http.Handler) http.Handler
|
LoggedIn(scopes ...Scope) func(next http.Handler) http.Handler
|
||||||
// UserIdMatch checks if the user id in the session matches the user id in the request. If not, the request is aborted.
|
// UserIdMatch checks if the user id in the session matches the user id in the request. If not, the request is aborted.
|
||||||
UserIdMatch(idParameter string) func(next http.Handler) http.Handler
|
UserIdMatch(idParameter string) func(next http.Handler) http.Handler
|
||||||
|
// InfoOnly only add user info to the request context. No login check is performed.
|
||||||
|
InfoOnly() func(next http.Handler) http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
type Session interface {
|
type Session interface {
|
||||||
|
@ -2,6 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -28,12 +29,54 @@ type AuthenticationService interface {
|
|||||||
OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error)
|
OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebAuthnService interface {
|
||||||
|
Enabled() bool
|
||||||
|
StartWebAuthnRegistration(ctx context.Context, userId domain.UserIdentifier) (
|
||||||
|
responseOptions []byte,
|
||||||
|
sessionData []byte,
|
||||||
|
err error,
|
||||||
|
)
|
||||||
|
FinishWebAuthnRegistration(
|
||||||
|
ctx context.Context,
|
||||||
|
userId domain.UserIdentifier,
|
||||||
|
name string,
|
||||||
|
sessionDataAsJSON []byte,
|
||||||
|
r *http.Request,
|
||||||
|
) ([]domain.UserWebauthnCredential, error)
|
||||||
|
GetCredentials(
|
||||||
|
ctx context.Context,
|
||||||
|
userId domain.UserIdentifier,
|
||||||
|
) ([]domain.UserWebauthnCredential, error)
|
||||||
|
RemoveCredential(
|
||||||
|
ctx context.Context,
|
||||||
|
userId domain.UserIdentifier,
|
||||||
|
credentialIdBase64 string,
|
||||||
|
) ([]domain.UserWebauthnCredential, error)
|
||||||
|
UpdateCredential(
|
||||||
|
ctx context.Context,
|
||||||
|
userId domain.UserIdentifier,
|
||||||
|
credentialIdBase64 string,
|
||||||
|
name string,
|
||||||
|
) ([]domain.UserWebauthnCredential, error)
|
||||||
|
StartWebAuthnLogin(_ context.Context) (
|
||||||
|
optionsAsJSON []byte,
|
||||||
|
sessionDataAsJSON []byte,
|
||||||
|
err error,
|
||||||
|
)
|
||||||
|
FinishWebAuthnLogin(
|
||||||
|
ctx context.Context,
|
||||||
|
sessionDataAsJSON []byte,
|
||||||
|
r *http.Request,
|
||||||
|
) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
type AuthEndpoint struct {
|
type AuthEndpoint struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
authService AuthenticationService
|
authService AuthenticationService
|
||||||
authenticator Authenticator
|
authenticator Authenticator
|
||||||
session Session
|
session Session
|
||||||
validate Validator
|
validate Validator
|
||||||
|
webAuthn WebAuthnService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthEndpoint(
|
func NewAuthEndpoint(
|
||||||
@ -42,6 +85,7 @@ func NewAuthEndpoint(
|
|||||||
session Session,
|
session Session,
|
||||||
validator Validator,
|
validator Validator,
|
||||||
authService AuthenticationService,
|
authService AuthenticationService,
|
||||||
|
webAuthn WebAuthnService,
|
||||||
) AuthEndpoint {
|
) AuthEndpoint {
|
||||||
return AuthEndpoint{
|
return AuthEndpoint{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@ -49,6 +93,7 @@ func NewAuthEndpoint(
|
|||||||
authenticator: authenticator,
|
authenticator: authenticator,
|
||||||
session: session,
|
session: session,
|
||||||
validate: validator,
|
validate: validator,
|
||||||
|
webAuthn: webAuthn,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +110,19 @@ func (e AuthEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
|||||||
apiGroup.HandleFunc("GET /login/{provider}/init", e.handleOauthInitiateGet())
|
apiGroup.HandleFunc("GET /login/{provider}/init", e.handleOauthInitiateGet())
|
||||||
apiGroup.HandleFunc("GET /login/{provider}/callback", e.handleOauthCallbackGet())
|
apiGroup.HandleFunc("GET /login/{provider}/callback", e.handleOauthCallbackGet())
|
||||||
|
|
||||||
|
apiGroup.HandleFunc("POST /webauthn/login/start", e.handleWebAuthnLoginStart())
|
||||||
|
apiGroup.HandleFunc("POST /webauthn/login/finish", e.handleWebAuthnLoginFinish())
|
||||||
|
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("GET /webauthn/credentials",
|
||||||
|
e.handleWebAuthnCredentialsGet())
|
||||||
|
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("POST /webauthn/register/start",
|
||||||
|
e.handleWebAuthnRegisterStart())
|
||||||
|
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("POST /webauthn/register/finish",
|
||||||
|
e.handleWebAuthnRegisterFinish())
|
||||||
|
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("DELETE /webauthn/credential/{id}",
|
||||||
|
e.handleWebAuthnCredentialsDelete())
|
||||||
|
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("PUT /webauthn/credential/{id}",
|
||||||
|
e.handleWebAuthnCredentialsPut())
|
||||||
|
|
||||||
apiGroup.HandleFunc("POST /login", e.handleLoginPost())
|
apiGroup.HandleFunc("POST /login", e.handleLoginPost())
|
||||||
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("POST /logout", e.handleLogoutPost())
|
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("POST /logout", e.handleLogoutPost())
|
||||||
}
|
}
|
||||||
@ -132,7 +190,7 @@ func (e AuthEndpoint) handleSessionInfoGet() http.HandlerFunc {
|
|||||||
// @Summary Initiate the OAuth login flow.
|
// @Summary Initiate the OAuth login flow.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} []model.LoginProviderInfo
|
// @Success 200 {object} []model.LoginProviderInfo
|
||||||
// @Router /auth/{provider}/init [get]
|
// @Router /auth/login/{provider}/init [get]
|
||||||
func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
currentSession := e.session.GetData(r.Context())
|
currentSession := e.session.GetData(r.Context())
|
||||||
@ -177,6 +235,8 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
|||||||
|
|
||||||
authCodeUrl, state, nonce, err := e.authService.OauthLoginStep1(context.Background(), provider)
|
authCodeUrl, state, nonce, err := e.authService.OauthLoginStep1(context.Background(), provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
slog.Debug("failed to create oauth auth code URL",
|
||||||
|
"provider", provider, "error", err)
|
||||||
if autoRedirect && e.isValidReturnUrl(returnTo) {
|
if autoRedirect && e.isValidReturnUrl(returnTo) {
|
||||||
redirectToReturn()
|
redirectToReturn()
|
||||||
} else {
|
} else {
|
||||||
@ -211,7 +271,7 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
|||||||
// @Summary Handle the OAuth callback.
|
// @Summary Handle the OAuth callback.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} []model.LoginProviderInfo
|
// @Success 200 {object} []model.LoginProviderInfo
|
||||||
// @Router /auth/{provider}/callback [get]
|
// @Router /auth/login/{provider}/callback [get]
|
||||||
func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
currentSession := e.session.GetData(r.Context())
|
currentSession := e.session.GetData(r.Context())
|
||||||
@ -249,6 +309,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
|||||||
oauthState := request.Query(r, "state")
|
oauthState := request.Query(r, "state")
|
||||||
|
|
||||||
if provider != currentSession.OauthProvider {
|
if provider != currentSession.OauthProvider {
|
||||||
|
slog.Debug("invalid oauth provider in callback",
|
||||||
|
"expected", currentSession.OauthProvider, "got", provider, "state", oauthState)
|
||||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||||
redirectToReturn()
|
redirectToReturn()
|
||||||
} else {
|
} else {
|
||||||
@ -258,6 +320,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if oauthState != currentSession.OauthState {
|
if oauthState != currentSession.OauthState {
|
||||||
|
slog.Debug("invalid oauth state in callback",
|
||||||
|
"expected", currentSession.OauthState, "got", oauthState, "provider", provider)
|
||||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||||
redirectToReturn()
|
redirectToReturn()
|
||||||
} else {
|
} else {
|
||||||
@ -267,11 +331,13 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loginCtx, cancel := context.WithTimeout(context.Background(), 1000*time.Second)
|
loginCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // avoid long waits
|
||||||
user, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce,
|
user, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce,
|
||||||
oauthCode)
|
oauthCode)
|
||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
slog.Debug("failed to process oauth code",
|
||||||
|
"provider", provider, "state", oauthState, "error", err)
|
||||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||||
redirectToReturn()
|
redirectToReturn()
|
||||||
} else {
|
} else {
|
||||||
@ -389,3 +455,237 @@ func (e AuthEndpoint) isValidReturnUrl(returnUrl string) bool {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleWebAuthnCredentialsGet returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID auth_handleWebAuthnCredentialsGet
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Summary Get all available external login providers.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []model.WebAuthnCredentialResponse
|
||||||
|
// @Router /auth/webauthn/credentials [get]
|
||||||
|
func (e AuthEndpoint) handleWebAuthnCredentialsGet() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !e.webAuthn.Enabled() {
|
||||||
|
respond.JSON(w, http.StatusOK, []model.WebAuthnCredentialResponse{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSession := e.session.GetData(r.Context())
|
||||||
|
|
||||||
|
userIdentifier := domain.UserIdentifier(currentSession.UserIdentifier)
|
||||||
|
|
||||||
|
credentials, err := e.webAuthn.GetCredentials(r.Context(), userIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.JSON(w, http.StatusOK, model.NewWebAuthnCredentialResponses(credentials))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleWebAuthnCredentialsDelete returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID auth_handleWebAuthnCredentialsDelete
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Summary Delete a WebAuthn credential.
|
||||||
|
// @Param id path string true "Base64 encoded Credential ID"
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []model.WebAuthnCredentialResponse
|
||||||
|
// @Router /auth/webauthn/credential/{id} [delete]
|
||||||
|
func (e AuthEndpoint) handleWebAuthnCredentialsDelete() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !e.webAuthn.Enabled() {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSession := e.session.GetData(r.Context())
|
||||||
|
|
||||||
|
userIdentifier := domain.UserIdentifier(currentSession.UserIdentifier)
|
||||||
|
|
||||||
|
credentialId := Base64UrlDecode(request.Path(r, "id"))
|
||||||
|
|
||||||
|
credentials, err := e.webAuthn.RemoveCredential(r.Context(), userIdentifier, credentialId)
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.JSON(w, http.StatusOK, model.NewWebAuthnCredentialResponses(credentials))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleWebAuthnCredentialsPut returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID auth_handleWebAuthnCredentialsPut
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Summary Update a WebAuthn credential.
|
||||||
|
// @Param id path string true "Base64 encoded Credential ID"
|
||||||
|
// @Param request body model.WebAuthnCredentialRequest true "Credential name"
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []model.WebAuthnCredentialResponse
|
||||||
|
// @Router /auth/webauthn/credential/{id} [put]
|
||||||
|
func (e AuthEndpoint) handleWebAuthnCredentialsPut() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !e.webAuthn.Enabled() {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSession := e.session.GetData(r.Context())
|
||||||
|
|
||||||
|
userIdentifier := domain.UserIdentifier(currentSession.UserIdentifier)
|
||||||
|
|
||||||
|
credentialId := Base64UrlDecode(request.Path(r, "id"))
|
||||||
|
var req model.WebAuthnCredentialRequest
|
||||||
|
if err := request.BodyJson(r, &req); err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials, err := e.webAuthn.UpdateCredential(r.Context(), userIdentifier, credentialId, req.Name)
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.JSON(w, http.StatusOK, model.NewWebAuthnCredentialResponses(credentials))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e AuthEndpoint) handleWebAuthnRegisterStart() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !e.webAuthn.Enabled() {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSession := e.session.GetData(r.Context())
|
||||||
|
|
||||||
|
userIdentifier := domain.UserIdentifier(currentSession.UserIdentifier)
|
||||||
|
|
||||||
|
options, sessionData, err := e.webAuthn.StartWebAuthnRegistration(r.Context(), userIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSession.WebAuthnData = string(sessionData)
|
||||||
|
e.session.SetData(r.Context(), currentSession)
|
||||||
|
|
||||||
|
respond.Data(w, http.StatusOK, "application/json", options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleWebAuthnRegisterFinish returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID auth_handleWebAuthnRegisterFinish
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Summary Finish the WebAuthn registration process.
|
||||||
|
// @Param credential_name query string false "Credential name" default("")
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []model.WebAuthnCredentialResponse
|
||||||
|
// @Router /auth/webauthn/register/finish [post]
|
||||||
|
func (e AuthEndpoint) handleWebAuthnRegisterFinish() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !e.webAuthn.Enabled() {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
name := request.QueryDefault(r, "credential_name", "")
|
||||||
|
|
||||||
|
currentSession := e.session.GetData(r.Context())
|
||||||
|
|
||||||
|
webAuthnSessionData := []byte(currentSession.WebAuthnData)
|
||||||
|
currentSession.WebAuthnData = "" // clear the session data
|
||||||
|
e.session.SetData(r.Context(), currentSession)
|
||||||
|
|
||||||
|
credentials, err := e.webAuthn.FinishWebAuthnRegistration(
|
||||||
|
r.Context(),
|
||||||
|
domain.UserIdentifier(currentSession.UserIdentifier),
|
||||||
|
name,
|
||||||
|
webAuthnSessionData,
|
||||||
|
r)
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.JSON(w, http.StatusOK, model.NewWebAuthnCredentialResponses(credentials))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e AuthEndpoint) handleWebAuthnLoginStart() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !e.webAuthn.Enabled() {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSession := e.session.GetData(r.Context())
|
||||||
|
|
||||||
|
options, sessionData, err := e.webAuthn.StartWebAuthnLogin(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSession.WebAuthnData = string(sessionData)
|
||||||
|
e.session.SetData(r.Context(), currentSession)
|
||||||
|
|
||||||
|
respond.Data(w, http.StatusOK, "application/json", options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleWebAuthnLoginFinish returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID auth_handleWebAuthnLoginFinish
|
||||||
|
// @Tags Authentication
|
||||||
|
// @Summary Finish the WebAuthn login process.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} model.User
|
||||||
|
// @Router /auth/webauthn/login/finish [post]
|
||||||
|
func (e AuthEndpoint) handleWebAuthnLoginFinish() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !e.webAuthn.Enabled() {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: "WebAuthn is not enabled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSession := e.session.GetData(r.Context())
|
||||||
|
|
||||||
|
webAuthnSessionData := []byte(currentSession.WebAuthnData)
|
||||||
|
currentSession.WebAuthnData = "" // clear the session data
|
||||||
|
e.session.SetData(r.Context(), currentSession)
|
||||||
|
|
||||||
|
user, err := e.webAuthn.FinishWebAuthnLogin(
|
||||||
|
r.Context(),
|
||||||
|
webAuthnSessionData,
|
||||||
|
r)
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.setAuthenticatedUser(r, user)
|
||||||
|
|
||||||
|
respond.JSON(w, http.StatusOK, model.NewUser(user, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/h44z/wg-portal/internal/app/api/core/respond"
|
"github.com/h44z/wg-portal/internal/app/api/core/respond"
|
||||||
"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"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed frontend_config.js.gotpl
|
//go:embed frontend_config.js.gotpl
|
||||||
@ -46,7 +47,7 @@ func (e ConfigEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
|||||||
apiGroup := g.Mount("/config")
|
apiGroup := g.Mount("/config")
|
||||||
|
|
||||||
apiGroup.HandleFunc("GET /frontend.js", e.handleConfigJsGet())
|
apiGroup.HandleFunc("GET /frontend.js", e.handleConfigJsGet())
|
||||||
apiGroup.With(e.authenticator.LoggedIn()).HandleFunc("GET /settings", e.handleSettingsGet())
|
apiGroup.With(e.authenticator.InfoOnly()).HandleFunc("GET /settings", e.handleSettingsGet())
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleConfigJsGet returns a gorm Handler function.
|
// handleConfigJsGet returns a gorm Handler function.
|
||||||
@ -93,11 +94,26 @@ func (e ConfigEndpoint) handleConfigJsGet() http.HandlerFunc {
|
|||||||
// @Router /config/settings [get]
|
// @Router /config/settings [get]
|
||||||
func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
|
func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
respond.JSON(w, http.StatusOK, model.Settings{
|
sessionUser := domain.GetUserInfo(r.Context())
|
||||||
MailLinkOnly: e.cfg.Mail.LinkOnly,
|
|
||||||
PersistentConfigSupported: e.cfg.Advanced.ConfigStoragePath != "",
|
hasSocialLogin := len(e.cfg.Auth.OAuth) > 0 || len(e.cfg.Auth.OpenIDConnect) > 0 || e.cfg.Auth.WebAuthn.Enabled
|
||||||
SelfProvisioning: e.cfg.Core.SelfProvisioningAllowed,
|
|
||||||
ApiAdminOnly: e.cfg.Advanced.ApiAdminOnly,
|
// For anonymous users, we return the settings object with minimal information
|
||||||
})
|
if sessionUser.Id == domain.CtxUnknownUserId || sessionUser.Id == "" {
|
||||||
|
respond.JSON(w, http.StatusOK, model.Settings{
|
||||||
|
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
|
||||||
|
LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
respond.JSON(w, http.StatusOK, model.Settings{
|
||||||
|
MailLinkOnly: e.cfg.Mail.LinkOnly,
|
||||||
|
PersistentConfigSupported: e.cfg.Advanced.ConfigStoragePath != "",
|
||||||
|
SelfProvisioning: e.cfg.Core.SelfProvisioningAllowed,
|
||||||
|
ApiAdminOnly: e.cfg.Advanced.ApiAdminOnly,
|
||||||
|
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
|
||||||
|
MinPasswordLength: e.cfg.Auth.MinPasswordLength,
|
||||||
|
LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,32 @@ func (h AuthenticationHandler) LoggedIn(scopes ...Scope) func(next http.Handler)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InfoOnly only checks if the user is logged in and adds the user id to the context.
|
||||||
|
// If the user is not logged in, the context user id is set to domain.CtxUnknownUserId.
|
||||||
|
func (h AuthenticationHandler) InfoOnly() func(next http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session := h.session.GetData(r.Context())
|
||||||
|
|
||||||
|
var newContext context.Context
|
||||||
|
|
||||||
|
if !session.LoggedIn {
|
||||||
|
newContext = domain.SetUserInfo(r.Context(), domain.DefaultContextUserInfo())
|
||||||
|
} else {
|
||||||
|
newContext = domain.SetUserInfo(r.Context(), &domain.ContextUserInfo{
|
||||||
|
Id: domain.UserIdentifier(session.UserIdentifier),
|
||||||
|
IsAdmin: session.IsAdmin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
r = r.WithContext(newContext)
|
||||||
|
|
||||||
|
// Continue down the chain to Handler etc
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UserIdMatch checks if the user id in the session matches the user id in the request. If not, the request is aborted.
|
// UserIdMatch checks if the user id in the session matches the user id in the request. If not, the request is aborted.
|
||||||
func (h AuthenticationHandler) UserIdMatch(idParameter string) func(next http.Handler) http.Handler {
|
func (h AuthenticationHandler) UserIdMatch(idParameter string) func(next http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
|
@ -31,6 +31,8 @@ type SessionData struct {
|
|||||||
OauthProvider string
|
OauthProvider string
|
||||||
OauthReturnTo string
|
OauthReturnTo string
|
||||||
|
|
||||||
|
WebAuthnData string
|
||||||
|
|
||||||
CsrfToken string
|
CsrfToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,4 +10,7 @@ type Settings struct {
|
|||||||
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
PersistentConfigSupported bool `json:"PersistentConfigSupported"`
|
||||||
SelfProvisioning bool `json:"SelfProvisioning"`
|
SelfProvisioning bool `json:"SelfProvisioning"`
|
||||||
ApiAdminOnly bool `json:"ApiAdminOnly"`
|
ApiAdminOnly bool `json:"ApiAdminOnly"`
|
||||||
|
WebAuthnEnabled bool `json:"WebAuthnEnabled"`
|
||||||
|
MinPasswordLength int `json:"MinPasswordLength"`
|
||||||
|
LoginFormVisible bool `json:"LoginFormVisible"`
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import "github.com/h44z/wg-portal/internal/domain"
|
import (
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
type LoginProviderInfo struct {
|
type LoginProviderInfo struct {
|
||||||
Identifier string `json:"Identifier" example:"google"`
|
Identifier string `json:"Identifier" example:"google"`
|
||||||
@ -39,3 +44,32 @@ type OauthInitiationResponse struct {
|
|||||||
RedirectUrl string
|
RedirectUrl string
|
||||||
State string
|
State string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebAuthnCredentialRequest struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
}
|
||||||
|
type WebAuthnCredentialResponse struct {
|
||||||
|
ID string `json:"ID"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
CreatedAt string `json:"CreatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebAuthnCredentialResponse(src domain.UserWebauthnCredential) WebAuthnCredentialResponse {
|
||||||
|
return WebAuthnCredentialResponse{
|
||||||
|
ID: src.CredentialIdentifier,
|
||||||
|
Name: src.DisplayName,
|
||||||
|
CreatedAt: src.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebAuthnCredentialResponses(src []domain.UserWebauthnCredential) []WebAuthnCredentialResponse {
|
||||||
|
credentials := make([]WebAuthnCredentialResponse, len(src))
|
||||||
|
for i := range src {
|
||||||
|
credentials[i] = NewWebAuthnCredentialResponse(src[i])
|
||||||
|
}
|
||||||
|
// Sort by CreatedAt, newest first
|
||||||
|
slices.SortFunc(credentials, func(i, j WebAuthnCredentialResponse) int {
|
||||||
|
return strings.Compare(i.CreatedAt, j.CreatedAt)
|
||||||
|
})
|
||||||
|
return credentials
|
||||||
|
}
|
||||||
|
@ -198,7 +198,7 @@ func NewPeerStats(enabled bool, src []domain.PeerStatus) *PeerStats {
|
|||||||
|
|
||||||
for _, srcStat := range src {
|
for _, srcStat := range src {
|
||||||
stats[string(srcStat.PeerId)] = PeerStatData{
|
stats[string(srcStat.PeerId)] = PeerStatData{
|
||||||
IsConnected: srcStat.IsConnected(),
|
IsConnected: srcStat.IsConnected,
|
||||||
IsPingable: srcStat.IsPingable,
|
IsPingable: srcStat.IsPingable,
|
||||||
LastPing: srcStat.LastPing,
|
LastPing: srcStat.LastPing,
|
||||||
BytesReceived: srcStat.BytesReceived,
|
BytesReceived: srcStat.BytesReceived,
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
type InterfaceServiceInterfaceManagerRepo interface {
|
type InterfaceServiceInterfaceManagerRepo interface {
|
||||||
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||||
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
PrepareInterface(ctx context.Context) (*domain.Interface, error)
|
||||||
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
|
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
|
||||||
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||||
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
|
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
|
||||||
@ -60,6 +61,19 @@ func (s InterfaceService) GetById(ctx context.Context, id domain.InterfaceIdenti
|
|||||||
return interfaceData, interfacePeers, nil
|
return interfaceData, interfacePeers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s InterfaceService) Prepare(ctx context.Context) (*domain.Interface, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceData, err := s.interfaces.PrepareInterface(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaceData, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s InterfaceService) Create(ctx context.Context, iface *domain.Interface) (*domain.Interface, error) {
|
func (s InterfaceService) Create(ctx context.Context, iface *domain.Interface) (*domain.Interface, error) {
|
||||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -13,6 +13,7 @@ type PeerServicePeerManagerRepo interface {
|
|||||||
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]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)
|
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
|
||||||
CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
|
CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
|
||||||
UpdatePeer(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
|
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||||
@ -95,6 +96,19 @@ func (s PeerService) GetById(ctx context.Context, id domain.PeerIdentifier) (*do
|
|||||||
return peer, nil
|
return peer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s PeerService) Prepare(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error) {
|
||||||
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := s.peers.PreparePeer(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return peer, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s PeerService) Create(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
|
func (s PeerService) Create(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
|
||||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
type InterfaceEndpointInterfaceService interface {
|
type InterfaceEndpointInterfaceService interface {
|
||||||
GetAll(context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
GetAll(context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||||
GetById(context.Context, domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
GetById(context.Context, domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
Prepare(context.Context) (*domain.Interface, error)
|
||||||
Create(context.Context, *domain.Interface) (*domain.Interface, error)
|
Create(context.Context, *domain.Interface) (*domain.Interface, error)
|
||||||
Update(context.Context, domain.InterfaceIdentifier, *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
Update(context.Context, domain.InterfaceIdentifier, *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||||
Delete(context.Context, domain.InterfaceIdentifier) error
|
Delete(context.Context, domain.InterfaceIdentifier) error
|
||||||
@ -49,6 +50,7 @@ func (e InterfaceEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
|||||||
apiGroup.HandleFunc("GET /all", e.handleAllGet())
|
apiGroup.HandleFunc("GET /all", e.handleAllGet())
|
||||||
apiGroup.HandleFunc("GET /by-id/{id}", e.handleByIdGet())
|
apiGroup.HandleFunc("GET /by-id/{id}", e.handleByIdGet())
|
||||||
|
|
||||||
|
apiGroup.HandleFunc("GET /prepare", e.handlePrepareGet())
|
||||||
apiGroup.HandleFunc("POST /new", e.handleCreatePost())
|
apiGroup.HandleFunc("POST /new", e.handleCreatePost())
|
||||||
apiGroup.HandleFunc("PUT /by-id/{id}", e.handleUpdatePut())
|
apiGroup.HandleFunc("PUT /by-id/{id}", e.handleUpdatePut())
|
||||||
apiGroup.HandleFunc("DELETE /by-id/{id}", e.handleDelete())
|
apiGroup.HandleFunc("DELETE /by-id/{id}", e.handleDelete())
|
||||||
@ -112,11 +114,38 @@ func (e InterfaceEndpoint) handleByIdGet() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handlePrepareGet returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handlePrepareGet
|
||||||
|
// @Tags Interfaces
|
||||||
|
// @Summary Prepare a new interface record.
|
||||||
|
// @Description This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} models.Interface
|
||||||
|
// @Failure 401 {object} models.Error
|
||||||
|
// @Failure 403 {object} models.Error
|
||||||
|
// @Failure 500 {object} models.Error
|
||||||
|
// @Router /interface/prepare [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e InterfaceEndpoint) handlePrepareGet() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
iface, err := e.interfaces.Prepare(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
status, model := ParseServiceError(err)
|
||||||
|
respond.JSON(w, status, model)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.JSON(w, http.StatusOK, models.NewInterface(iface, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// handleCreatePost returns a gorm handler function.
|
// handleCreatePost returns a gorm handler function.
|
||||||
//
|
//
|
||||||
// @ID interfaces_handleCreatePost
|
// @ID interfaces_handleCreatePost
|
||||||
// @Tags Interfaces
|
// @Tags Interfaces
|
||||||
// @Summary Create a new interface record.
|
// @Summary Create a new interface record.
|
||||||
|
// @Description This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
|
||||||
// @Param request body models.Interface true "The interface data."
|
// @Param request body models.Interface true "The interface data."
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} models.Interface
|
// @Success 200 {object} models.Interface
|
||||||
@ -155,6 +184,7 @@ func (e InterfaceEndpoint) handleCreatePost() http.HandlerFunc {
|
|||||||
// @ID interfaces_handleUpdatePut
|
// @ID interfaces_handleUpdatePut
|
||||||
// @Tags Interfaces
|
// @Tags Interfaces
|
||||||
// @Summary Update an interface record.
|
// @Summary Update an interface record.
|
||||||
|
// @Description This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
|
||||||
// @Param id path string true "The interface identifier."
|
// @Param id path string true "The interface identifier."
|
||||||
// @Param request body models.Interface true "The interface data."
|
// @Param request body models.Interface true "The interface data."
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
@ -16,6 +16,7 @@ type PeerService interface {
|
|||||||
GetForInterface(context.Context, domain.InterfaceIdentifier) ([]domain.Peer, error)
|
GetForInterface(context.Context, domain.InterfaceIdentifier) ([]domain.Peer, error)
|
||||||
GetForUser(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
|
GetForUser(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
GetById(context.Context, domain.PeerIdentifier) (*domain.Peer, error)
|
GetById(context.Context, domain.PeerIdentifier) (*domain.Peer, error)
|
||||||
|
Prepare(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
|
||||||
Create(context.Context, *domain.Peer) (*domain.Peer, error)
|
Create(context.Context, *domain.Peer) (*domain.Peer, error)
|
||||||
Update(context.Context, domain.PeerIdentifier, *domain.Peer) (*domain.Peer, error)
|
Update(context.Context, domain.PeerIdentifier, *domain.Peer) (*domain.Peer, error)
|
||||||
Delete(context.Context, domain.PeerIdentifier) error
|
Delete(context.Context, domain.PeerIdentifier) error
|
||||||
@ -51,6 +52,7 @@ func (e PeerEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
|||||||
apiGroup.HandleFunc("GET /by-user/{id}", e.handleAllForUserGet())
|
apiGroup.HandleFunc("GET /by-user/{id}", e.handleAllForUserGet())
|
||||||
apiGroup.HandleFunc("GET /by-id/{id}", e.handleByIdGet())
|
apiGroup.HandleFunc("GET /by-id/{id}", e.handleByIdGet())
|
||||||
|
|
||||||
|
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("GET /prepare/{id}", e.handlePrepareGet())
|
||||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /new", e.handleCreatePost())
|
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /new", e.handleCreatePost())
|
||||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("PUT /by-id/{id}", e.handleUpdatePut())
|
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("PUT /by-id/{id}", e.handleUpdatePut())
|
||||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("DELETE /by-id/{id}", e.handleDelete())
|
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("DELETE /by-id/{id}", e.handleDelete())
|
||||||
@ -156,12 +158,48 @@ func (e PeerEndpoint) handleByIdGet() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handlePrepareGet returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handlePrepareGet
|
||||||
|
// @Tags Peers
|
||||||
|
// @Summary Prepare a new peer record for the given WireGuard interface.
|
||||||
|
// @Description This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.
|
||||||
|
// @Param id path string true "The interface identifier."
|
||||||
|
// @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/prepare/{id} [get]
|
||||||
|
// @Security BasicAuth
|
||||||
|
func (e PeerEndpoint) handlePrepareGet() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := request.Path(r, "id")
|
||||||
|
if id == "" {
|
||||||
|
respond.JSON(w, http.StatusBadRequest,
|
||||||
|
models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := e.peers.Prepare(r.Context(), domain.InterfaceIdentifier(id))
|
||||||
|
if err != nil {
|
||||||
|
status, model := ParseServiceError(err)
|
||||||
|
respond.JSON(w, status, model)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.JSON(w, http.StatusOK, models.NewPeer(peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// handleCreatePost returns a gorm handler function.
|
// handleCreatePost returns a gorm handler function.
|
||||||
//
|
//
|
||||||
// @ID peers_handleCreatePost
|
// @ID peers_handleCreatePost
|
||||||
// @Tags Peers
|
// @Tags Peers
|
||||||
// @Summary Create a new peer record.
|
// @Summary Create a new peer record.
|
||||||
// @Description Only admins can create new records.
|
// @Description Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).
|
||||||
// @Param request body models.Peer true "The peer data."
|
// @Param request body models.Peer true "The peer data."
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} models.Peer
|
// @Success 200 {object} models.Peer
|
||||||
@ -200,7 +238,7 @@ func (e PeerEndpoint) handleCreatePost() http.HandlerFunc {
|
|||||||
// @ID peers_handleUpdatePut
|
// @ID peers_handleUpdatePut
|
||||||
// @Tags Peers
|
// @Tags Peers
|
||||||
// @Summary Update a peer record.
|
// @Summary Update a peer record.
|
||||||
// @Description Only admins can update existing records.
|
// @Description Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).
|
||||||
// @Param id path string true "The peer identifier."
|
// @Param id path string true "The peer identifier."
|
||||||
// @Param request body models.Peer true "The peer data."
|
// @Param request body models.Peer true "The peer data."
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
@ -63,6 +63,8 @@ type AuthenticatorOauth interface {
|
|||||||
ParseUserInfo(raw map[string]any) (*domain.AuthenticatorUserInfo, error)
|
ParseUserInfo(raw map[string]any) (*domain.AuthenticatorUserInfo, error)
|
||||||
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
|
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
|
||||||
RegistrationEnabled() bool
|
RegistrationEnabled() bool
|
||||||
|
// GetAllowedDomains returns the list of whitelisted domains
|
||||||
|
GetAllowedDomains() []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthenticatorLdap is the interface for all LDAP authenticators.
|
// AuthenticatorLdap is the interface for all LDAP authenticators.
|
||||||
@ -392,6 +394,23 @@ func (a *Authenticator) randString(nByte int) (string, error) {
|
|||||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isDomainAllowed(email string, allowedDomains []string) bool {
|
||||||
|
if len(allowedDomains) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
parts := strings.Split(email, "@")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
domain := strings.ToLower(parts[1])
|
||||||
|
for _, allowed := range allowedDomains {
|
||||||
|
if domain == strings.ToLower(allowed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and
|
// OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and
|
||||||
// fetching the user information.
|
// fetching the user information.
|
||||||
func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error) {
|
func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error) {
|
||||||
@ -431,6 +450,10 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
|
|||||||
return nil, fmt.Errorf("unable to process user information: %w", err)
|
return nil, fmt.Errorf("unable to process user information: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isDomainAllowed(userInfo.Email, oauthProvider.GetAllowedDomains()) {
|
||||||
|
return nil, fmt.Errorf("user is not in allowed domains: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if user.IsLocked() || user.IsDisabled() {
|
if user.IsLocked() || user.IsDisabled() {
|
||||||
a.bus.Publish(app.TopicAuditLoginFailed, domain.AuditEventWrapper[audit.AuthEvent]{
|
a.bus.Publish(app.TopicAuditLoginFailed, domain.AuditEventWrapper[audit.AuthEvent]{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
|
@ -27,6 +27,7 @@ type PlainOauthAuthenticator struct {
|
|||||||
userAdminMapping *config.OauthAdminMapping
|
userAdminMapping *config.OauthAdminMapping
|
||||||
registrationEnabled bool
|
registrationEnabled bool
|
||||||
userInfoLogging bool
|
userInfoLogging bool
|
||||||
|
allowedDomains []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPlainOauthAuthenticator(
|
func newPlainOauthAuthenticator(
|
||||||
@ -56,6 +57,7 @@ func newPlainOauthAuthenticator(
|
|||||||
provider.userAdminMapping = &cfg.AdminMapping
|
provider.userAdminMapping = &cfg.AdminMapping
|
||||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||||
provider.userInfoLogging = cfg.LogUserInfo
|
provider.userInfoLogging = cfg.LogUserInfo
|
||||||
|
provider.allowedDomains = cfg.AllowedDomains
|
||||||
|
|
||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
@ -65,6 +67,10 @@ func (p PlainOauthAuthenticator) GetName() string {
|
|||||||
return p.name
|
return p.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p PlainOauthAuthenticator) GetAllowedDomains() []string {
|
||||||
|
return p.allowedDomains
|
||||||
|
}
|
||||||
|
|
||||||
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
|
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
|
||||||
func (p PlainOauthAuthenticator) RegistrationEnabled() bool {
|
func (p PlainOauthAuthenticator) RegistrationEnabled() bool {
|
||||||
return p.registrationEnabled
|
return p.registrationEnabled
|
||||||
|
@ -24,6 +24,7 @@ type OidcAuthenticator struct {
|
|||||||
userAdminMapping *config.OauthAdminMapping
|
userAdminMapping *config.OauthAdminMapping
|
||||||
registrationEnabled bool
|
registrationEnabled bool
|
||||||
userInfoLogging bool
|
userInfoLogging bool
|
||||||
|
allowedDomains []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOidcAuthenticator(
|
func newOidcAuthenticator(
|
||||||
@ -57,6 +58,7 @@ func newOidcAuthenticator(
|
|||||||
provider.userAdminMapping = &cfg.AdminMapping
|
provider.userAdminMapping = &cfg.AdminMapping
|
||||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||||
provider.userInfoLogging = cfg.LogUserInfo
|
provider.userInfoLogging = cfg.LogUserInfo
|
||||||
|
provider.allowedDomains = cfg.AllowedDomains
|
||||||
|
|
||||||
return provider, nil
|
return provider, nil
|
||||||
}
|
}
|
||||||
@ -66,6 +68,10 @@ func (o OidcAuthenticator) GetName() string {
|
|||||||
return o.name
|
return o.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o OidcAuthenticator) GetAllowedDomains() []string {
|
||||||
|
return o.allowedDomains
|
||||||
|
}
|
||||||
|
|
||||||
// RegistrationEnabled returns whether registration is enabled for this authenticator.
|
// RegistrationEnabled returns whether registration is enabled for this authenticator.
|
||||||
func (o OidcAuthenticator) RegistrationEnabled() bool {
|
func (o OidcAuthenticator) RegistrationEnabled() bool {
|
||||||
return o.registrationEnabled
|
return o.registrationEnabled
|
||||||
|
301
internal/app/auth/webauthn.go
Normal file
301
internal/app/auth/webauthn.go
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/go-webauthn/webauthn/protocol"
|
||||||
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/audit"
|
||||||
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebAuthnUserManager interface {
|
||||||
|
// GetUser returns a user by its identifier.
|
||||||
|
GetUser(context.Context, domain.UserIdentifier) (*domain.User, error)
|
||||||
|
// GetUserByWebAuthnCredential returns a user by its WebAuthn ID.
|
||||||
|
GetUserByWebAuthnCredential(ctx context.Context, credentialIdBase64 string) (*domain.User, error)
|
||||||
|
// UpdateUser updates an existing user in the database.
|
||||||
|
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebAuthnAuthenticator struct {
|
||||||
|
webAuthn *webauthn.WebAuthn
|
||||||
|
users WebAuthnUserManager
|
||||||
|
bus EventBus
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebAuthnAuthenticator(cfg *config.Config, bus EventBus, users WebAuthnUserManager) (
|
||||||
|
*WebAuthnAuthenticator,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
if !cfg.Auth.WebAuthn.Enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
extUrl, err := url.Parse(cfg.Web.ExternalUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to parse external URL - required for WebAuthn RP ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
rpId := extUrl.Hostname()
|
||||||
|
if rpId == "" {
|
||||||
|
return nil, errors.New("failed to determine Webauthn RPID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the WebAuthn authenticator with the provided configuration
|
||||||
|
awCfg := &webauthn.Config{
|
||||||
|
RPID: rpId,
|
||||||
|
RPDisplayName: cfg.Web.SiteTitle,
|
||||||
|
RPOrigins: []string{cfg.Web.ExternalUrl},
|
||||||
|
}
|
||||||
|
|
||||||
|
webAuthn, err := webauthn.New(awCfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create Webauthn instance: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WebAuthnAuthenticator{
|
||||||
|
webAuthn: webAuthn,
|
||||||
|
users: users,
|
||||||
|
bus: bus,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WebAuthnAuthenticator) Enabled() bool {
|
||||||
|
return a != nil && a.webAuthn != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WebAuthnAuthenticator) StartWebAuthnRegistration(ctx context.Context, userId domain.UserIdentifier) (
|
||||||
|
optionsAsJSON []byte,
|
||||||
|
sessionDataAsJSON []byte,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
user, err := a.users.GetUser(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to get user: %w", err)
|
||||||
|
}
|
||||||
|
if user.IsLocked() || user.IsDisabled() {
|
||||||
|
return nil, nil, errors.New("user is locked") // adding passkey to locked user is not allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.WebAuthnId == "" {
|
||||||
|
user.GenerateWebAuthnId()
|
||||||
|
user, err = a.users.UpdateUser(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to store webauthn id to user: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options, sessionData, err := a.webAuthn.BeginRegistration(user,
|
||||||
|
webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to begin WebAuthn registration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsAsJSON, err = json.Marshal(options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to marshal webauthn options to JSON: %w", err)
|
||||||
|
}
|
||||||
|
sessionDataAsJSON, err = json.Marshal(sessionData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to marshal webauthn session data to JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionsAsJSON, sessionDataAsJSON, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WebAuthnAuthenticator) FinishWebAuthnRegistration(
|
||||||
|
ctx context.Context,
|
||||||
|
userId domain.UserIdentifier,
|
||||||
|
name string,
|
||||||
|
sessionDataAsJSON []byte,
|
||||||
|
r *http.Request,
|
||||||
|
) ([]domain.UserWebauthnCredential, error) {
|
||||||
|
user, err := a.users.GetUser(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||||
|
}
|
||||||
|
if user.IsLocked() || user.IsDisabled() {
|
||||||
|
return nil, errors.New("user is locked") // adding passkey to locked user is not allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
var webAuthnData webauthn.SessionData
|
||||||
|
err = json.Unmarshal(sessionDataAsJSON, &webAuthnData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal webauthn session data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
credential, err := a.webAuthn.FinishRegistration(user, webAuthnData, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
name = fmt.Sprintf("Passkey %d", len(user.WebAuthnCredentialList)+1) // fallback name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the credential to the user
|
||||||
|
err = user.AddCredential(userId, name, *credential)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err = a.users.UpdateUser(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.WebAuthnCredentialList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WebAuthnAuthenticator) GetCredentials(
|
||||||
|
ctx context.Context,
|
||||||
|
userId domain.UserIdentifier,
|
||||||
|
) ([]domain.UserWebauthnCredential, error) {
|
||||||
|
user, err := a.users.GetUser(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.WebAuthnCredentialList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WebAuthnAuthenticator) RemoveCredential(
|
||||||
|
ctx context.Context,
|
||||||
|
userId domain.UserIdentifier,
|
||||||
|
credentialIdBase64 string,
|
||||||
|
) ([]domain.UserWebauthnCredential, error) {
|
||||||
|
user, err := a.users.GetUser(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.RemoveCredential(credentialIdBase64)
|
||||||
|
user, err = a.users.UpdateUser(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.WebAuthnCredentialList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WebAuthnAuthenticator) UpdateCredential(
|
||||||
|
ctx context.Context,
|
||||||
|
userId domain.UserIdentifier,
|
||||||
|
credentialIdBase64 string,
|
||||||
|
name string,
|
||||||
|
) ([]domain.UserWebauthnCredential, error) {
|
||||||
|
user, err := a.users.GetUser(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = user.UpdateCredential(credentialIdBase64, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err = a.users.UpdateUser(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.WebAuthnCredentialList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WebAuthnAuthenticator) StartWebAuthnLogin(_ context.Context) (
|
||||||
|
optionsAsJSON []byte,
|
||||||
|
sessionDataAsJSON []byte,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
options, sessionData, err := a.webAuthn.BeginDiscoverableLogin()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to begin WebAuthn login: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsAsJSON, err = json.Marshal(options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to marshal webauthn options to JSON: %w", err)
|
||||||
|
}
|
||||||
|
sessionDataAsJSON, err = json.Marshal(sessionData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to marshal webauthn session data to JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionsAsJSON, sessionDataAsJSON, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WebAuthnAuthenticator) FinishWebAuthnLogin(
|
||||||
|
ctx context.Context,
|
||||||
|
sessionDataAsJSON []byte,
|
||||||
|
r *http.Request,
|
||||||
|
) (*domain.User, error) {
|
||||||
|
|
||||||
|
var webAuthnData webauthn.SessionData
|
||||||
|
err := json.Unmarshal(sessionDataAsJSON, &webAuthnData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal webauthn session data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// switch to admin context for user lookup
|
||||||
|
ctx = domain.SetUserInfo(ctx, domain.SystemAdminContextUserInfo())
|
||||||
|
|
||||||
|
credential, err := a.webAuthn.FinishDiscoverableLogin(a.findUserForWebAuthnSecretFn(ctx), webAuthnData, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the user by the WebAuthn ID
|
||||||
|
user, err := a.users.GetUserByWebAuthnCredential(ctx,
|
||||||
|
base64.StdEncoding.EncodeToString(credential.ID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user by webauthn credential: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.IsLocked() || user.IsDisabled() {
|
||||||
|
a.bus.Publish(app.TopicAuditLoginFailed, domain.AuditEventWrapper[audit.AuthEvent]{
|
||||||
|
Ctx: ctx,
|
||||||
|
Source: "passkey",
|
||||||
|
Event: audit.AuthEvent{
|
||||||
|
Username: string(user.Identifier), Error: "User is locked",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return nil, errors.New("user is locked") // login with passkey is not allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
a.bus.Publish(app.TopicAuthLogin, user.Identifier)
|
||||||
|
a.bus.Publish(app.TopicAuditLoginSuccess, domain.AuditEventWrapper[audit.AuthEvent]{
|
||||||
|
Ctx: ctx,
|
||||||
|
Source: "passkey",
|
||||||
|
Event: audit.AuthEvent{
|
||||||
|
Username: string(user.Identifier),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *WebAuthnAuthenticator) findUserForWebAuthnSecretFn(ctx context.Context) func(rawID, userHandle []byte) (
|
||||||
|
user webauthn.User,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
return func(rawID, userHandle []byte) (webauthn.User, error) {
|
||||||
|
// Find the user by the WebAuthn ID
|
||||||
|
user, err := a.users.GetUserByWebAuthnCredential(ctx, base64.StdEncoding.EncodeToString(rawID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get user by webauthn credential: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
}
|
@ -36,6 +36,7 @@ const TopicPeerDeleted = "peer:deleted"
|
|||||||
const TopicPeerUpdated = "peer:updated"
|
const TopicPeerUpdated = "peer:updated"
|
||||||
const TopicPeerInterfaceUpdated = "peer:interface:updated"
|
const TopicPeerInterfaceUpdated = "peer:interface:updated"
|
||||||
const TopicPeerIdentifierUpdated = "peer:identifier:updated"
|
const TopicPeerIdentifierUpdated = "peer:identifier:updated"
|
||||||
|
const TopicPeerStateChanged = "peer:state:changed"
|
||||||
|
|
||||||
// endregion peer-events
|
// endregion peer-events
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ func NewMailManager(
|
|||||||
users UserDatabaseRepo,
|
users UserDatabaseRepo,
|
||||||
wg WireguardDatabaseRepo,
|
wg WireguardDatabaseRepo,
|
||||||
) (*Manager, error) {
|
) (*Manager, error) {
|
||||||
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl)
|
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl, cfg.Web.SiteTitle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize template handler: %w", err)
|
return nil, fmt.Errorf("failed to initialize template handler: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -17,11 +17,12 @@ var TemplateFiles embed.FS
|
|||||||
// TemplateHandler is a struct that holds the html and text templates.
|
// TemplateHandler is a struct that holds the html and text templates.
|
||||||
type TemplateHandler struct {
|
type TemplateHandler struct {
|
||||||
portalUrl string
|
portalUrl string
|
||||||
|
portalName string
|
||||||
htmlTemplates *htmlTemplate.Template
|
htmlTemplates *htmlTemplate.Template
|
||||||
textTemplates *template.Template
|
textTemplates *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTemplateHandler(portalUrl string) (*TemplateHandler, error) {
|
func newTemplateHandler(portalUrl, portalName string) (*TemplateHandler, error) {
|
||||||
htmlTemplateCache, err := htmlTemplate.New("Html").ParseFS(TemplateFiles, "tpl_files/*.gohtml")
|
htmlTemplateCache, err := htmlTemplate.New("Html").ParseFS(TemplateFiles, "tpl_files/*.gohtml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse html template files: %w", err)
|
return nil, fmt.Errorf("failed to parse html template files: %w", err)
|
||||||
@ -34,6 +35,7 @@ func newTemplateHandler(portalUrl string) (*TemplateHandler, error) {
|
|||||||
|
|
||||||
handler := &TemplateHandler{
|
handler := &TemplateHandler{
|
||||||
portalUrl: portalUrl,
|
portalUrl: portalUrl,
|
||||||
|
portalName: portalName,
|
||||||
htmlTemplates: htmlTemplateCache,
|
htmlTemplates: htmlTemplateCache,
|
||||||
textTemplates: txtTemplateCache,
|
textTemplates: txtTemplateCache,
|
||||||
}
|
}
|
||||||
@ -81,6 +83,7 @@ func (c TemplateHandler) GetConfigMailWithAttachment(user *domain.User, cfgName,
|
|||||||
"ConfigFileName": cfgName,
|
"ConfigFileName": cfgName,
|
||||||
"QrcodePngName": qrName,
|
"QrcodePngName": qrName,
|
||||||
"PortalUrl": c.portalUrl,
|
"PortalUrl": c.portalUrl,
|
||||||
|
"PortalName": c.portalName,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gotpl: %w", err)
|
return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gotpl: %w", err)
|
||||||
@ -91,6 +94,7 @@ func (c TemplateHandler) GetConfigMailWithAttachment(user *domain.User, cfgName,
|
|||||||
"ConfigFileName": cfgName,
|
"ConfigFileName": cfgName,
|
||||||
"QrcodePngName": qrName,
|
"QrcodePngName": qrName,
|
||||||
"PortalUrl": c.portalUrl,
|
"PortalUrl": c.portalUrl,
|
||||||
|
"PortalName": c.portalName,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gohtml: %w", err)
|
return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gohtml: %w", err)
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<!--[if !mso]><!-->
|
<!--[if !mso]><!-->
|
||||||
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
||||||
<!--<![endif]-->
|
<!--<![endif]-->
|
||||||
<title>Email Template</title>
|
<title>{{$.PortalName}}</title>
|
||||||
<!--[if gte mso 9]>
|
<!--[if gte mso 9]>
|
||||||
<style type="text/css" media="all">
|
<style type="text/css" media="all">
|
||||||
sup { font-size: 100% !important; }
|
sup { font-size: 100% !important; }
|
||||||
@ -143,7 +143,7 @@
|
|||||||
<td align="left">
|
<td align="left">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="blue-button text-button" style="background:#000000; color:#c1cddc; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
|
<td class="blue-button text-button" style="background:#000000; color:#ffffff; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
@ -167,10 +167,10 @@
|
|||||||
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#ffffff">
|
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#ffffff">
|
||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated using WireGuard Portal.</td>
|
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated by {{$.PortalName}}.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit WireGuard Portal</span></a></td>
|
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit {{$.PortalName}}</span></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
@ -20,5 +20,5 @@ You can download and install the WireGuard VPN client from:
|
|||||||
https://www.wireguard.com/install/
|
https://www.wireguard.com/install/
|
||||||
|
|
||||||
|
|
||||||
This mail was generated using WireGuard Portal.
|
This mail was generated by {{$.PortalName}}.
|
||||||
{{$.PortalUrl}}
|
{{$.PortalUrl}}
|
@ -19,7 +19,7 @@
|
|||||||
<!--[if !mso]><!-->
|
<!--[if !mso]><!-->
|
||||||
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
||||||
<!--<![endif]-->
|
<!--<![endif]-->
|
||||||
<title>Email Template</title>
|
<title>{{$.PortalName}}</title>
|
||||||
<!--[if gte mso 9]>
|
<!--[if gte mso 9]>
|
||||||
<style type="text/css" media="all">
|
<style type="text/css" media="all">
|
||||||
sup { font-size: 100% !important; }
|
sup { font-size: 100% !important; }
|
||||||
@ -143,7 +143,7 @@
|
|||||||
<td align="left">
|
<td align="left">
|
||||||
<table border="0" cellspacing="0" cellpadding="0">
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="blue-button text-button" style="background:#000000; color:#c1cddc; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
|
<td class="blue-button text-button" style="background:#000000; color:#ffffff; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
@ -167,10 +167,10 @@
|
|||||||
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#ffffff">
|
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#ffffff">
|
||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated using WireGuard Portal.</td>
|
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated by {{$.PortalName}}.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit WireGuard Portal</span></a></td>
|
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit {{$.PortalName}}</span></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
|
@ -20,5 +20,5 @@ You can download and install the WireGuard VPN client from:
|
|||||||
https://www.wireguard.com/install/
|
https://www.wireguard.com/install/
|
||||||
|
|
||||||
|
|
||||||
This mail was generated using WireGuard Portal.
|
This mail was generated by {{$.PortalName}}.
|
||||||
{{$.PortalUrl}}
|
{{$.PortalUrl}}
|
@ -25,6 +25,8 @@ type UserDatabaseRepo interface {
|
|||||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||||
// GetUserByEmail returns the user with the given email address.
|
// GetUserByEmail returns the user with the given email address.
|
||||||
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
|
||||||
|
// GetUserByWebAuthnCredential returns the user for the given WebAuthn credential ID.
|
||||||
|
GetUserByWebAuthnCredential(ctx context.Context, credentialIdBase64 string) (*domain.User, error)
|
||||||
// GetAllUsers returns all users.
|
// GetAllUsers returns all users.
|
||||||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||||
// FindUsers returns all users matching the search string.
|
// FindUsers returns all users matching the search string.
|
||||||
@ -129,6 +131,25 @@ func (m Manager) GetUserByEmail(ctx context.Context, email string) (*domain.User
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserByWebAuthnCredential returns the user for the given WebAuthn credential.
|
||||||
|
func (m Manager) GetUserByWebAuthnCredential(ctx context.Context, credentialIdBase64 string) (*domain.User, error) {
|
||||||
|
|
||||||
|
user, err := m.users.GetUserByWebAuthnCredential(ctx, credentialIdBase64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to load user for webauthn credential %s: %w", credentialIdBase64, 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
|
||||||
|
}
|
||||||
|
|
||||||
// GetAllUsers returns all users.
|
// GetAllUsers returns all users.
|
||||||
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 {
|
||||||
@ -343,6 +364,10 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use
|
|||||||
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := new.HasWeakPassword(m.cfg.Auth.MinPasswordLength); err != nil {
|
||||||
|
return errors.Join(fmt.Errorf("password too weak: %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: %w", domain.ErrInvalidData)
|
return fmt.Errorf("cannot remove own admin rights: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
@ -397,7 +422,11 @@ func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
|
|||||||
|
|
||||||
// database users must have a password
|
// database users must have a password
|
||||||
if new.Source == domain.UserSourceDatabase && string(new.Password) == "" {
|
if new.Source == domain.UserSourceDatabase && string(new.Password) == "" {
|
||||||
return fmt.Errorf("invalid password: %w", domain.ErrInvalidData)
|
return fmt.Errorf("missing password: %w", domain.ErrInvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := new.HasWeakPassword(m.cfg.Auth.MinPasswordLength); err != nil {
|
||||||
|
return errors.Join(fmt.Errorf("password too weak: %w", err), domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -64,6 +64,7 @@ func (m Manager) connectToMessageBus() {
|
|||||||
_ = m.bus.Subscribe(app.TopicPeerCreated, m.handlePeerCreateEvent)
|
_ = m.bus.Subscribe(app.TopicPeerCreated, m.handlePeerCreateEvent)
|
||||||
_ = m.bus.Subscribe(app.TopicPeerUpdated, m.handlePeerUpdateEvent)
|
_ = m.bus.Subscribe(app.TopicPeerUpdated, m.handlePeerUpdateEvent)
|
||||||
_ = m.bus.Subscribe(app.TopicPeerDeleted, m.handlePeerDeleteEvent)
|
_ = m.bus.Subscribe(app.TopicPeerDeleted, m.handlePeerDeleteEvent)
|
||||||
|
_ = m.bus.Subscribe(app.TopicPeerStateChanged, m.handlePeerStateChangeEvent)
|
||||||
|
|
||||||
_ = m.bus.Subscribe(app.TopicInterfaceCreated, m.handleInterfaceCreateEvent)
|
_ = m.bus.Subscribe(app.TopicInterfaceCreated, m.handleInterfaceCreateEvent)
|
||||||
_ = m.bus.Subscribe(app.TopicInterfaceUpdated, m.handleInterfaceUpdateEvent)
|
_ = m.bus.Subscribe(app.TopicInterfaceUpdated, m.handleInterfaceUpdateEvent)
|
||||||
@ -135,6 +136,14 @@ func (m Manager) handleInterfaceDeleteEvent(iface domain.Interface) {
|
|||||||
m.handleGenericEvent(WebhookEventDelete, iface)
|
m.handleGenericEvent(WebhookEventDelete, iface)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Manager) handlePeerStateChangeEvent(peerStatus domain.PeerStatus) {
|
||||||
|
if peerStatus.IsConnected {
|
||||||
|
m.handleGenericEvent(WebhookEventConnect, peerStatus)
|
||||||
|
} else {
|
||||||
|
m.handleGenericEvent(WebhookEventDisconnect, peerStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m Manager) handleGenericEvent(action WebhookEvent, payload any) {
|
func (m Manager) handleGenericEvent(action WebhookEvent, payload any) {
|
||||||
eventData, err := m.createWebhookData(action, payload)
|
eventData, err := m.createWebhookData(action, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -177,6 +186,9 @@ func (m Manager) createWebhookData(action WebhookEvent, payload any) (*WebhookDa
|
|||||||
case domain.Interface:
|
case domain.Interface:
|
||||||
d.Entity = WebhookEntityInterface
|
d.Entity = WebhookEntityInterface
|
||||||
d.Identifier = string(v.Identifier)
|
d.Identifier = string(v.Identifier)
|
||||||
|
case domain.PeerStatus:
|
||||||
|
d.Entity = WebhookEntityPeer
|
||||||
|
d.Identifier = string(v.PeerId)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported payload type: %T", v)
|
return nil, fmt.Errorf("unsupported payload type: %T", v)
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,9 @@ const (
|
|||||||
type WebhookEvent = string
|
type WebhookEvent = string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
WebhookEventCreate WebhookEvent = "create"
|
WebhookEventCreate WebhookEvent = "create"
|
||||||
WebhookEventUpdate WebhookEvent = "update"
|
WebhookEventUpdate WebhookEvent = "update"
|
||||||
WebhookEventDelete WebhookEvent = "delete"
|
WebhookEventDelete WebhookEvent = "delete"
|
||||||
|
WebhookEventConnect WebhookEvent = "connect"
|
||||||
|
WebhookEventDisconnect WebhookEvent = "disconnect"
|
||||||
)
|
)
|
||||||
|
@ -43,6 +43,8 @@ type StatisticsMetricsServer interface {
|
|||||||
type StatisticsEventBus interface {
|
type StatisticsEventBus interface {
|
||||||
// Subscribe subscribes to a topic
|
// Subscribe subscribes to a topic
|
||||||
Subscribe(topic string, fn interface{}) error
|
Subscribe(topic string, fn interface{}) error
|
||||||
|
// Publish sends a message to the message bus.
|
||||||
|
Publish(topic string, args ...any)
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatisticsCollector struct {
|
type StatisticsCollector struct {
|
||||||
@ -55,6 +57,8 @@ type StatisticsCollector struct {
|
|||||||
db StatisticsDatabaseRepo
|
db StatisticsDatabaseRepo
|
||||||
wg StatisticsInterfaceController
|
wg StatisticsInterfaceController
|
||||||
ms StatisticsMetricsServer
|
ms StatisticsMetricsServer
|
||||||
|
|
||||||
|
peerChangeEvent chan domain.PeerIdentifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStatisticsCollector creates a new statistics collector.
|
// NewStatisticsCollector creates a new statistics collector.
|
||||||
@ -171,8 +175,12 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
|
var connectionStateChanged bool
|
||||||
|
var newPeerStatus domain.PeerStatus
|
||||||
err = c.db.UpdatePeerStatus(ctx, peer.Identifier,
|
err = c.db.UpdatePeerStatus(ctx, peer.Identifier,
|
||||||
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
|
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
|
||||||
|
wasConnected := p.IsConnected
|
||||||
|
|
||||||
var lastHandshake *time.Time
|
var lastHandshake *time.Time
|
||||||
if !peer.LastHandshake.IsZero() {
|
if !peer.LastHandshake.IsZero() {
|
||||||
lastHandshake = &peer.LastHandshake
|
lastHandshake = &peer.LastHandshake
|
||||||
@ -186,6 +194,12 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server
|
p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server
|
||||||
p.Endpoint = peer.Endpoint
|
p.Endpoint = peer.Endpoint
|
||||||
p.LastHandshake = lastHandshake
|
p.LastHandshake = lastHandshake
|
||||||
|
p.CalcConnected()
|
||||||
|
|
||||||
|
if wasConnected != p.IsConnected {
|
||||||
|
connectionStateChanged = true
|
||||||
|
newPeerStatus = *p // store new status for event publishing
|
||||||
|
}
|
||||||
|
|
||||||
// Update prometheus metrics
|
// Update prometheus metrics
|
||||||
go c.updatePeerMetrics(ctx, *p)
|
go c.updatePeerMetrics(ctx, *p)
|
||||||
@ -197,6 +211,11 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
} else {
|
} else {
|
||||||
slog.Debug("updated peer status", "peer", peer.Identifier)
|
slog.Debug("updated peer status", "peer", peer.Identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if connectionStateChanged {
|
||||||
|
// publish event if connection state changed
|
||||||
|
c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -298,12 +317,17 @@ func (c *StatisticsCollector) enqueuePingChecks(ctx context.Context) {
|
|||||||
func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
||||||
defer c.pingWaitGroup.Done()
|
defer c.pingWaitGroup.Done()
|
||||||
for peer := range c.pingJobs {
|
for peer := range c.pingJobs {
|
||||||
|
var connectionStateChanged bool
|
||||||
|
var newPeerStatus domain.PeerStatus
|
||||||
|
|
||||||
peerPingable := c.isPeerPingable(ctx, peer)
|
peerPingable := c.isPeerPingable(ctx, peer)
|
||||||
slog.Debug("peer ping check completed", "peer", peer.Identifier, "pingable", peerPingable)
|
slog.Debug("peer ping check completed", "peer", peer.Identifier, "pingable", peerPingable)
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
err := c.db.UpdatePeerStatus(ctx, peer.Identifier,
|
err := c.db.UpdatePeerStatus(ctx, peer.Identifier,
|
||||||
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
|
func(p *domain.PeerStatus) (*domain.PeerStatus, error) {
|
||||||
|
wasConnected := p.IsConnected
|
||||||
|
|
||||||
if peerPingable {
|
if peerPingable {
|
||||||
p.IsPingable = true
|
p.IsPingable = true
|
||||||
p.LastPing = &now
|
p.LastPing = &now
|
||||||
@ -311,6 +335,13 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
|||||||
p.IsPingable = false
|
p.IsPingable = false
|
||||||
p.LastPing = nil
|
p.LastPing = nil
|
||||||
}
|
}
|
||||||
|
p.UpdatedAt = time.Now()
|
||||||
|
p.CalcConnected()
|
||||||
|
|
||||||
|
if wasConnected != p.IsConnected {
|
||||||
|
connectionStateChanged = true
|
||||||
|
newPeerStatus = *p // store new status for event publishing
|
||||||
|
}
|
||||||
|
|
||||||
// Update prometheus metrics
|
// Update prometheus metrics
|
||||||
go c.updatePeerMetrics(ctx, *p)
|
go c.updatePeerMetrics(ctx, *p)
|
||||||
@ -322,6 +353,11 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) {
|
|||||||
} else {
|
} else {
|
||||||
slog.Debug("updated peer ping status", "peer", peer.Identifier)
|
slog.Debug("updated peer ping status", "peer", peer.Identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if connectionStateChanged {
|
||||||
|
// publish event if connection state changed
|
||||||
|
c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package wireguard
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
@ -76,6 +77,8 @@ type Manager struct {
|
|||||||
db InterfaceAndPeerDatabaseRepo
|
db InterfaceAndPeerDatabaseRepo
|
||||||
wg InterfaceController
|
wg InterfaceController
|
||||||
quick WgQuickController
|
quick WgQuickController
|
||||||
|
|
||||||
|
userLockMap *sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWireGuardManager(
|
func NewWireGuardManager(
|
||||||
@ -86,11 +89,12 @@ func NewWireGuardManager(
|
|||||||
db InterfaceAndPeerDatabaseRepo,
|
db InterfaceAndPeerDatabaseRepo,
|
||||||
) (*Manager, error) {
|
) (*Manager, error) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
bus: bus,
|
bus: bus,
|
||||||
wg: wg,
|
wg: wg,
|
||||||
db: db,
|
db: db,
|
||||||
quick: quick,
|
quick: quick,
|
||||||
|
userLockMap: &sync.Map{},
|
||||||
}
|
}
|
||||||
|
|
||||||
m.connectToMessageBus()
|
m.connectToMessageBus()
|
||||||
@ -117,6 +121,12 @@ func (m Manager) handleUserCreationEvent(user domain.User) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, loaded := m.userLockMap.LoadOrStore(user.Identifier, "create")
|
||||||
|
if loaded {
|
||||||
|
return // another goroutine is already handling this user
|
||||||
|
}
|
||||||
|
defer m.userLockMap.Delete(user.Identifier)
|
||||||
|
|
||||||
slog.Debug("handling new user event", "user", user.Identifier)
|
slog.Debug("handling new user event", "user", user.Identifier)
|
||||||
|
|
||||||
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
|
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
|
||||||
@ -132,6 +142,12 @@ func (m Manager) handleUserLoginEvent(userId domain.UserIdentifier) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, loaded := m.userLockMap.LoadOrStore(userId, "login")
|
||||||
|
if loaded {
|
||||||
|
return // another goroutine is already handling this user
|
||||||
|
}
|
||||||
|
defer m.userLockMap.Delete(userId)
|
||||||
|
|
||||||
userPeers, err := m.db.GetUserPeers(context.Background(), userId)
|
userPeers, err := m.db.GetUserPeers(context.Background(), userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to retrieve existing peers prior to default peer creation",
|
slog.Error("failed to retrieve existing peers prior to default peer creation",
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
@ -23,12 +24,24 @@ func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdenti
|
|||||||
return fmt.Errorf("failed to fetch all interfaces: %w", err)
|
return fmt.Errorf("failed to fetch all interfaces: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userPeers, err := m.db.GetUserPeers(context.Background(), userId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve existing peers prior to default peer creation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var newPeers []domain.Peer
|
var newPeers []domain.Peer
|
||||||
for _, iface := range existingInterfaces {
|
for _, iface := range existingInterfaces {
|
||||||
if iface.Type != domain.InterfaceTypeServer {
|
if iface.Type != domain.InterfaceTypeServer {
|
||||||
continue // only create default peers for server interfaces
|
continue // only create default peers for server interfaces
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peerAlreadyCreated := slices.ContainsFunc(userPeers, func(peer domain.Peer) bool {
|
||||||
|
return peer.InterfaceIdentifier == iface.Identifier
|
||||||
|
})
|
||||||
|
if peerAlreadyCreated {
|
||||||
|
continue // skip creation if a peer already exists for this interface
|
||||||
|
}
|
||||||
|
|
||||||
peer, err := m.PreparePeer(ctx, iface.Identifier)
|
peer, err := m.PreparePeer(ctx, iface.Identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create default peer for interface %s: %w", iface.Identifier, err)
|
return fmt.Errorf("failed to create default peer for interface %s: %w", iface.Identifier, err)
|
||||||
@ -175,6 +188,30 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
|
|||||||
|
|
||||||
sessionUser := domain.GetUserInfo(ctx)
|
sessionUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
|
// Enforce peer limit for non-admin users if LimitAdditionalUserPeers is set
|
||||||
|
if m.cfg.Core.SelfProvisioningAllowed && !sessionUser.IsAdmin && m.cfg.Advanced.LimitAdditionalUserPeers > 0 {
|
||||||
|
peers, err := m.db.GetUserPeers(ctx, peer.UserIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch peers for user %s: %w", peer.UserIdentifier, err)
|
||||||
|
}
|
||||||
|
// Count enabled peers (disabled IS NULL)
|
||||||
|
peerCount := 0
|
||||||
|
for _, p := range peers {
|
||||||
|
if !p.IsDisabled() {
|
||||||
|
peerCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalAllowedPeers := 1 + m.cfg.Advanced.LimitAdditionalUserPeers // 1 default peer + x additional peers
|
||||||
|
if peerCount >= totalAllowedPeers {
|
||||||
|
slog.WarnContext(ctx, "peer creation blocked due to limit",
|
||||||
|
"user", peer.UserIdentifier,
|
||||||
|
"current_count", peerCount,
|
||||||
|
"allowed_count", totalAllowedPeers)
|
||||||
|
return nil, fmt.Errorf("peer limit reached (%d peers allowed): %w", totalAllowedPeers, domain.ErrNoPermission)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
|
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
|
||||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||||
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)
|
||||||
|
@ -16,6 +16,14 @@ type Auth struct {
|
|||||||
OAuth []OAuthProvider `yaml:"oauth"`
|
OAuth []OAuthProvider `yaml:"oauth"`
|
||||||
// Ldap contains a list of LDAP providers.
|
// Ldap contains a list of LDAP providers.
|
||||||
Ldap []LdapProvider `yaml:"ldap"`
|
Ldap []LdapProvider `yaml:"ldap"`
|
||||||
|
// Webauthn contains the configuration for the WebAuthn authenticator.
|
||||||
|
WebAuthn WebauthnConfig `yaml:"webauthn"`
|
||||||
|
// MinPasswordLength is the minimum password length for user accounts. This also applies to the admin user.
|
||||||
|
// It is encouraged to set this value to at least 16 characters.
|
||||||
|
MinPasswordLength int `yaml:"min_password_length"`
|
||||||
|
// HideLoginForm specifies whether the login form should be hidden. If no social login providers are configured,
|
||||||
|
// the login form will be shown regardless of this setting.
|
||||||
|
HideLoginForm bool `yaml:"hide_login_form"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BaseFields contains the basic fields that are used to map user information from the authentication providers.
|
// BaseFields contains the basic fields that are used to map user information from the authentication providers.
|
||||||
@ -188,6 +196,9 @@ type OpenIDConnectProvider struct {
|
|||||||
// ExtraScopes specifies optional requested permissions.
|
// ExtraScopes specifies optional requested permissions.
|
||||||
ExtraScopes []string `yaml:"extra_scopes"`
|
ExtraScopes []string `yaml:"extra_scopes"`
|
||||||
|
|
||||||
|
// AllowedDomains defines the list of allowed domains
|
||||||
|
AllowedDomains []string `yaml:"allowed_domains"`
|
||||||
|
|
||||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||||
FieldMap OauthFields `yaml:"field_map"`
|
FieldMap OauthFields `yaml:"field_map"`
|
||||||
|
|
||||||
@ -226,6 +237,9 @@ type OAuthProvider struct {
|
|||||||
// Scope specifies optional requested permissions.
|
// Scope specifies optional requested permissions.
|
||||||
Scopes []string `yaml:"scopes"`
|
Scopes []string `yaml:"scopes"`
|
||||||
|
|
||||||
|
// AllowedDomains defines the list of allowed domains
|
||||||
|
AllowedDomains []string `yaml:"allowed_domains"`
|
||||||
|
|
||||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||||
FieldMap OauthFields `yaml:"field_map"`
|
FieldMap OauthFields `yaml:"field_map"`
|
||||||
|
|
||||||
@ -239,3 +253,9 @@ type OAuthProvider struct {
|
|||||||
// If LogUserInfo is set to true, the user info retrieved from the OAuth provider will be logged in trace level.
|
// If LogUserInfo is set to true, the user info retrieved from the OAuth provider will be logged in trace level.
|
||||||
LogUserInfo bool `yaml:"log_user_info"`
|
LogUserInfo bool `yaml:"log_user_info"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebauthnConfig contains the configuration for the WebAuthn authenticator.
|
||||||
|
type WebauthnConfig struct {
|
||||||
|
// Enabled specifies whether WebAuthn is enabled.
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
}
|
||||||
|
@ -29,18 +29,19 @@ type Config struct {
|
|||||||
} `yaml:"core"`
|
} `yaml:"core"`
|
||||||
|
|
||||||
Advanced struct {
|
Advanced struct {
|
||||||
LogLevel string `yaml:"log_level"`
|
LogLevel string `yaml:"log_level"`
|
||||||
LogPretty bool `yaml:"log_pretty"`
|
LogPretty bool `yaml:"log_pretty"`
|
||||||
LogJson bool `yaml:"log_json"`
|
LogJson bool `yaml:"log_json"`
|
||||||
StartListenPort int `yaml:"start_listen_port"`
|
StartListenPort int `yaml:"start_listen_port"`
|
||||||
StartCidrV4 string `yaml:"start_cidr_v4"`
|
StartCidrV4 string `yaml:"start_cidr_v4"`
|
||||||
StartCidrV6 string `yaml:"start_cidr_v6"`
|
StartCidrV6 string `yaml:"start_cidr_v6"`
|
||||||
UseIpV6 bool `yaml:"use_ip_v6"`
|
UseIpV6 bool `yaml:"use_ip_v6"`
|
||||||
ConfigStoragePath string `yaml:"config_storage_path"` // keep empty to disable config export to file
|
ConfigStoragePath string `yaml:"config_storage_path"` // keep empty to disable config export to file
|
||||||
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
|
ApiAdminOnly bool `yaml:"api_admin_only"` // if true, only admin users can access the API
|
||||||
|
LimitAdditionalUserPeers int `yaml:"limit_additional_user_peers"`
|
||||||
} `yaml:"advanced"`
|
} `yaml:"advanced"`
|
||||||
|
|
||||||
Statistics struct {
|
Statistics struct {
|
||||||
@ -76,6 +77,7 @@ func (c *Config) LogStartupValues() {
|
|||||||
"reEnablePeerAfterUserEnable", c.Core.ReEnablePeerAfterUserEnable,
|
"reEnablePeerAfterUserEnable", c.Core.ReEnablePeerAfterUserEnable,
|
||||||
"deletePeerAfterUserDeleted", c.Core.DeletePeerAfterUserDeleted,
|
"deletePeerAfterUserDeleted", c.Core.DeletePeerAfterUserDeleted,
|
||||||
"selfProvisioningAllowed", c.Core.SelfProvisioningAllowed,
|
"selfProvisioningAllowed", c.Core.SelfProvisioningAllowed,
|
||||||
|
"limitAdditionalUserPeers", c.Advanced.LimitAdditionalUserPeers,
|
||||||
"importExisting", c.Core.ImportExisting,
|
"importExisting", c.Core.ImportExisting,
|
||||||
"restoreState", c.Core.RestoreState,
|
"restoreState", c.Core.RestoreState,
|
||||||
"useIpV6", c.Advanced.UseIpV6,
|
"useIpV6", c.Advanced.UseIpV6,
|
||||||
@ -93,6 +95,9 @@ func (c *Config) LogStartupValues() {
|
|||||||
"oidcProviders", len(c.Auth.OpenIDConnect),
|
"oidcProviders", len(c.Auth.OpenIDConnect),
|
||||||
"oauthProviders", len(c.Auth.OAuth),
|
"oauthProviders", len(c.Auth.OAuth),
|
||||||
"ldapProviders", len(c.Auth.Ldap),
|
"ldapProviders", len(c.Auth.Ldap),
|
||||||
|
"webauthnEnabled", c.Auth.WebAuthn.Enabled,
|
||||||
|
"minPasswordLength", c.Auth.MinPasswordLength,
|
||||||
|
"hideLoginForm", c.Auth.HideLoginForm,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +106,7 @@ func defaultConfig() *Config {
|
|||||||
cfg := &Config{}
|
cfg := &Config{}
|
||||||
|
|
||||||
cfg.Core.AdminUser = "admin@wgportal.local"
|
cfg.Core.AdminUser = "admin@wgportal.local"
|
||||||
cfg.Core.AdminPassword = "wgportal"
|
cfg.Core.AdminPassword = "wgportal-default"
|
||||||
cfg.Core.AdminApiToken = "" // by default, the API access is disabled
|
cfg.Core.AdminApiToken = "" // by default, the API access is disabled
|
||||||
cfg.Core.ImportExisting = true
|
cfg.Core.ImportExisting = true
|
||||||
cfg.Core.RestoreState = true
|
cfg.Core.RestoreState = true
|
||||||
@ -137,6 +142,7 @@ func defaultConfig() *Config {
|
|||||||
cfg.Advanced.RulePrioOffset = 20000
|
cfg.Advanced.RulePrioOffset = 20000
|
||||||
cfg.Advanced.RouteTableOffset = 20000
|
cfg.Advanced.RouteTableOffset = 20000
|
||||||
cfg.Advanced.ApiAdminOnly = true
|
cfg.Advanced.ApiAdminOnly = true
|
||||||
|
cfg.Advanced.LimitAdditionalUserPeers = 0
|
||||||
|
|
||||||
cfg.Statistics.UsePingChecks = true
|
cfg.Statistics.UsePingChecks = true
|
||||||
cfg.Statistics.PingCheckWorkers = 10
|
cfg.Statistics.PingCheckWorkers = 10
|
||||||
@ -164,6 +170,10 @@ func defaultConfig() *Config {
|
|||||||
cfg.Webhook.Authentication = ""
|
cfg.Webhook.Authentication = ""
|
||||||
cfg.Webhook.Timeout = 10 * time.Second
|
cfg.Webhook.Timeout = 10 * time.Second
|
||||||
|
|
||||||
|
cfg.Auth.WebAuthn.Enabled = true
|
||||||
|
cfg.Auth.MinPasswordLength = 16
|
||||||
|
cfg.Auth.HideLoginForm = false
|
||||||
|
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,6 +200,8 @@ func GetConfig() (*Config, error) {
|
|||||||
return nil, fmt.Errorf("failed to load config from yaml: %w", err)
|
return nil, fmt.Errorf("failed to load config from yaml: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.Web.Sanitize()
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
// WebConfig contains the configuration for the web server.
|
// WebConfig contains the configuration for the web server.
|
||||||
type WebConfig struct {
|
type WebConfig struct {
|
||||||
// RequestLogging enables logging of all HTTP requests.
|
// RequestLogging enables logging of all HTTP requests.
|
||||||
@ -26,3 +28,7 @@ type WebConfig struct {
|
|||||||
// KeyFile is the path to the TLS certificate key file.
|
// KeyFile is the path to the TLS certificate key file.
|
||||||
KeyFile string `yaml:"key_file"`
|
KeyFile string `yaml:"key_file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *WebConfig) Sanitize() {
|
||||||
|
c.ExternalUrl = strings.TrimRight(c.ExternalUrl, "/")
|
||||||
|
}
|
||||||
|
@ -136,6 +136,7 @@ func (p *Peer) OverwriteUserEditableFields(userPeer *Peer, cfg *config.Config) {
|
|||||||
p.Interface.PublicKey = userPeer.Interface.PublicKey
|
p.Interface.PublicKey = userPeer.Interface.PublicKey
|
||||||
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
|
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
|
||||||
p.PresharedKey = userPeer.PresharedKey
|
p.PresharedKey = userPeer.PresharedKey
|
||||||
|
p.Identifier = userPeer.Identifier
|
||||||
}
|
}
|
||||||
p.Interface.Mtu = userPeer.Interface.Mtu
|
p.Interface.Mtu = userPeer.Interface.Mtu
|
||||||
p.PersistentKeepalive = userPeer.PersistentKeepalive
|
p.PersistentKeepalive = userPeer.PersistentKeepalive
|
||||||
|
@ -3,21 +3,23 @@ package domain
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type PeerStatus struct {
|
type PeerStatus struct {
|
||||||
PeerId PeerIdentifier `gorm:"primaryKey;column:identifier"`
|
PeerId PeerIdentifier `gorm:"primaryKey;column:identifier" json:"PeerId"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"-"`
|
||||||
|
|
||||||
IsPingable bool `gorm:"column:pingable"`
|
IsConnected bool `gorm:"column:connected" json:"IsConnected"` // indicates if the peer is connected based on the last handshake or ping
|
||||||
LastPing *time.Time `gorm:"column:last_ping"`
|
|
||||||
|
|
||||||
BytesReceived uint64 `gorm:"column:received"`
|
IsPingable bool `gorm:"column:pingable" json:"IsPingable"`
|
||||||
BytesTransmitted uint64 `gorm:"column:transmitted"`
|
LastPing *time.Time `gorm:"column:last_ping" json:"LastPing"`
|
||||||
|
|
||||||
LastHandshake *time.Time `gorm:"column:last_handshake"`
|
BytesReceived uint64 `gorm:"column:received" json:"BytesReceived"`
|
||||||
Endpoint string `gorm:"column:endpoint"`
|
BytesTransmitted uint64 `gorm:"column:transmitted" json:"BytesTransmitted"`
|
||||||
LastSessionStart *time.Time `gorm:"column:last_session_start"`
|
|
||||||
|
LastHandshake *time.Time `gorm:"column:last_handshake" json:"LastHandshake"`
|
||||||
|
Endpoint string `gorm:"column:endpoint" json:"Endpoint"`
|
||||||
|
LastSessionStart *time.Time `gorm:"column:last_session_start" json:"LastSessionStart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s PeerStatus) IsConnected() bool {
|
func (s *PeerStatus) CalcConnected() {
|
||||||
oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
|
oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
|
||||||
|
|
||||||
handshakeValid := false
|
handshakeValid := false
|
||||||
@ -25,7 +27,7 @@ func (s PeerStatus) IsConnected() bool {
|
|||||||
handshakeValid = !s.LastHandshake.Before(oldestHandshakeTime)
|
handshakeValid = !s.LastHandshake.Before(oldestHandshakeTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.IsPingable || handshakeValid
|
s.IsConnected = s.IsPingable || handshakeValid
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceStatus struct {
|
type InterfaceStatus struct {
|
||||||
|
@ -66,8 +66,9 @@ func TestPeerStatus_IsConnected(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if got := tt.status.IsConnected(); got != tt.want {
|
tt.status.CalcConnected()
|
||||||
t.Errorf("IsConnected() = %v, want %v", got, tt.want)
|
if got := tt.status.IsConnected; got != tt.want {
|
||||||
|
t.Errorf("IsConnected = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,16 @@ package domain
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,6 +50,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
|
||||||
|
|
||||||
|
// Passwordless authentication
|
||||||
|
WebAuthnId string `gorm:"column:webauthn_id"` // the webauthn id of the user, used for webauthn authentication
|
||||||
|
WebAuthnCredentialList []UserWebauthnCredential `gorm:"foreignKey:user_identifier"` // the webauthn credentials of the user, used for webauthn authentication
|
||||||
|
|
||||||
// API token for REST API access
|
// API token for REST API access
|
||||||
ApiToken string `form:"api_token" binding:"omitempty"`
|
ApiToken string `form:"api_token" binding:"omitempty"`
|
||||||
ApiTokenCreated *time.Time
|
ApiTokenCreated *time.Time
|
||||||
@ -77,6 +88,22 @@ func (u *User) CanChangePassword() error {
|
|||||||
return errors.New("password change only allowed for database source")
|
return errors.New("password change only allowed for database source")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) HasWeakPassword(minLength int) error {
|
||||||
|
if u.Source != UserSourceDatabase {
|
||||||
|
return nil // password is not required for non-database users, so no check needed
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Password == "" {
|
||||||
|
return nil // password is not set, so no check needed
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(u.Password) < minLength {
|
||||||
|
return fmt.Errorf("password is too short, minimum length is %d", minLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil // password is strong enough
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) EditAllowed(new *User) error {
|
func (u *User) EditAllowed(new *User) error {
|
||||||
if u.Source == UserSourceDatabase {
|
if u.Source == UserSourceDatabase {
|
||||||
return nil
|
return nil
|
||||||
@ -157,3 +184,148 @@ func (u *User) CopyCalculatedAttributes(src *User) {
|
|||||||
u.BaseModel = src.BaseModel
|
u.BaseModel = src.BaseModel
|
||||||
u.LinkedPeerCount = src.LinkedPeerCount
|
u.LinkedPeerCount = src.LinkedPeerCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// region webauthn
|
||||||
|
|
||||||
|
func (u *User) WebAuthnID() []byte {
|
||||||
|
decodeString, err := base64.StdEncoding.DecodeString(u.WebAuthnId)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeString
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) GenerateWebAuthnId() {
|
||||||
|
randomUid1 := uuid.New().String() // 32 hex digits + 4 dashes
|
||||||
|
randomUid2 := uuid.New().String() // 32 hex digits + 4 dashes
|
||||||
|
webAuthnId := []byte(strings.ReplaceAll(fmt.Sprintf("%s%s", randomUid1, randomUid2), "-", "")) // 64 hex digits
|
||||||
|
|
||||||
|
u.WebAuthnId = base64.StdEncoding.EncodeToString(webAuthnId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) WebAuthnName() string {
|
||||||
|
return string(u.Identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) WebAuthnDisplayName() string {
|
||||||
|
var userName string
|
||||||
|
switch {
|
||||||
|
case u.Firstname != "" && u.Lastname != "":
|
||||||
|
userName = fmt.Sprintf("%s %s", u.Firstname, u.Lastname)
|
||||||
|
case u.Firstname != "":
|
||||||
|
userName = u.Firstname
|
||||||
|
case u.Lastname != "":
|
||||||
|
userName = u.Lastname
|
||||||
|
default:
|
||||||
|
userName = string(u.Identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) WebAuthnCredentials() []webauthn.Credential {
|
||||||
|
credentials := make([]webauthn.Credential, len(u.WebAuthnCredentialList))
|
||||||
|
for i, cred := range u.WebAuthnCredentialList {
|
||||||
|
credential, err := cred.GetCredential()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
credentials[i] = credential
|
||||||
|
}
|
||||||
|
return credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) AddCredential(userId UserIdentifier, name string, credential webauthn.Credential) error {
|
||||||
|
cred, err := NewUserWebauthnCredential(userId, name, credential)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the credential already exists
|
||||||
|
for _, c := range u.WebAuthnCredentialList {
|
||||||
|
if c.GetCredentialId() == string(credential.ID) {
|
||||||
|
return errors.New("credential already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u.WebAuthnCredentialList = append(u.WebAuthnCredentialList, cred)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) UpdateCredential(credentialIdBase64, name string) error {
|
||||||
|
for i, c := range u.WebAuthnCredentialList {
|
||||||
|
if c.CredentialIdentifier == credentialIdBase64 {
|
||||||
|
u.WebAuthnCredentialList[i].DisplayName = name
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("credential not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) RemoveCredential(credentialIdBase64 string) {
|
||||||
|
u.WebAuthnCredentialList = slices.DeleteFunc(u.WebAuthnCredentialList, func(e UserWebauthnCredential) bool {
|
||||||
|
return e.CredentialIdentifier == credentialIdBase64
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserWebauthnCredential struct {
|
||||||
|
UserIdentifier string `gorm:"primaryKey;column:user_identifier"` // the user identifier
|
||||||
|
CredentialIdentifier string `gorm:"primaryKey;uniqueIndex;column:credential_identifier"` // base64 encoded credential id
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at"` // the time when the credential was created
|
||||||
|
DisplayName string `gorm:"column:display_name"` // the display name of the credential
|
||||||
|
SerializedCredential string `gorm:"column:serialized_credential"` // JSON and base64 encoded credential
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserWebauthnCredential(userIdentifier UserIdentifier, name string, credential webauthn.Credential) (
|
||||||
|
UserWebauthnCredential,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
c := UserWebauthnCredential{
|
||||||
|
UserIdentifier: string(userIdentifier),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
DisplayName: name,
|
||||||
|
CredentialIdentifier: base64.StdEncoding.EncodeToString(credential.ID),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.SetCredential(credential)
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserWebauthnCredential) SetCredential(credential webauthn.Credential) error {
|
||||||
|
jsonData, err := json.Marshal(credential)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal credential: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SerializedCredential = base64.StdEncoding.EncodeToString(jsonData)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserWebauthnCredential) GetCredential() (webauthn.Credential, error) {
|
||||||
|
jsonData, err := base64.StdEncoding.DecodeString(c.SerializedCredential)
|
||||||
|
if err != nil {
|
||||||
|
return webauthn.Credential{}, fmt.Errorf("failed to decode base64 credential: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var credential webauthn.Credential
|
||||||
|
if err := json.Unmarshal(jsonData, &credential); err != nil {
|
||||||
|
return webauthn.Credential{}, fmt.Errorf("failed to unmarshal credential: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return credential, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UserWebauthnCredential) GetCredentialId() string {
|
||||||
|
decodeString, _ := base64.StdEncoding.DecodeString(c.CredentialIdentifier)
|
||||||
|
|
||||||
|
return string(decodeString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion webauthn
|
||||||
|
@ -78,6 +78,11 @@ nav:
|
|||||||
- Configuration:
|
- Configuration:
|
||||||
- Overview: documentation/configuration/overview.md
|
- Overview: documentation/configuration/overview.md
|
||||||
- Examples: documentation/configuration/examples.md
|
- Examples: documentation/configuration/examples.md
|
||||||
|
- Usage:
|
||||||
|
- General: documentation/usage/general.md
|
||||||
|
- LDAP: documentation/usage/ldap.md
|
||||||
|
- Security: documentation/usage/security.md
|
||||||
|
- Webhooks: documentation/usage/webhooks.md
|
||||||
|
- REST API: documentation/rest-api/api-doc.md
|
||||||
- Upgrade: documentation/upgrade/v1.md
|
- Upgrade: documentation/upgrade/v1.md
|
||||||
- Monitoring: documentation/monitoring/prometheus.md
|
- Monitoring: documentation/monitoring/prometheus.md
|
||||||
- REST API: documentation/rest-api/api-doc.md
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user