Compare commits

..

1 Commits

Author SHA1 Message Date
Christoph Haas
4c986cc74c feat: add support for PKCE (#686) 2026-05-26 22:47:38 +02:00
19 changed files with 214 additions and 738 deletions

View File

@@ -135,8 +135,7 @@ func main() {
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers) apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth, wireGuard) apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth, wireGuard)
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth) apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
apiV0EndpointWebsocket := handlersV0.NewWebsocketEndpoint(cfg, apiV0Auth, eventBus, apiV0BackendPeers) apiV0EndpointWebsocket := handlersV0.NewWebsocketEndpoint(cfg, apiV0Auth, eventBus)
apiV0EndpointWebsocket.StartBackgroundJobs(ctx)
apiFrontend := handlersV0.NewRestApi(apiV0Session, apiFrontend := handlersV0.NewRestApi(apiV0Session,
apiV0EndpointAuth, apiV0EndpointAuth,

View File

@@ -552,7 +552,6 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
#### `provider_name` #### `provider_name`
- **Default:** *(empty)* - **Default:** *(empty)*
- **Description:** A **unique** name for this provider. Must not conflict with other providers. - **Description:** A **unique** name for this provider. Must not conflict with other providers.
This name is used to derive the callback URL for the OIDC provider: `<external_url>/api/v0/auth/login/<provider_name>/callback`.
#### `display_name` #### `display_name`
- **Default:** *(empty)* - **Default:** *(empty)*
@@ -640,7 +639,6 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
#### `provider_name` #### `provider_name`
- **Default:** *(empty)* - **Default:** *(empty)*
- **Description:** A **unique** name for this provider. Must not conflict with other providers. - **Description:** A **unique** name for this provider. Must not conflict with other providers.
This name is used to derive the callback URL for the OAuth provider: `<external_url>/api/v0/auth/login/<provider_name>/callback`.
#### `display_name` #### `display_name`
- **Default:** *(empty)* - **Default:** *(empty)*

View File

@@ -51,31 +51,13 @@ sudo install wg-portal /opt/wg-portal/
To handle tasks such as restarting the service or configuring automatic startup, it is recommended to use a process manager like [systemd](https://systemd.io/). To handle tasks such as restarting the service or configuring automatic startup, it is recommended to use a process manager like [systemd](https://systemd.io/).
Refer to [Systemd Service Setup](#systemd-service-setup) for instructions. Refer to [Systemd Service Setup](#systemd-service-setup) for instructions.
## Systemd Integration ## Systemd Service Setup
> **Note:** To run WireGuard Portal as systemd service, you need to download the binary for your architecture beforehand. > **Note:** To run WireGuard Portal as systemd service, you need to download the binary for your architecture beforehand.
> >
> The following examples assume that you downloaded the binary to `/opt/wg-portal/wg-portal`. > The following examples assume that you downloaded the binary to `/opt/wg-portal/wg-portal`.
> The configuration file is expected to be located at `/opt/wg-portal/config.yml`. > The configuration file is expected to be located at `/opt/wg-portal/config.yml`.
### Limit Systemd-Networkd Management Scope
If you are using `systemd-networkd` to manage the rest of your network
configuration, you will need to ensure it doesn't remove routing policy
created by `wg-portal` when it restarts:
```shell
sudo mkdir --parents /etc/systemd/networkd.conf.d/
sudo tee --append /etc/systemd/networkd.conf.d/foreign-routing.conf <<EOF
[Network]
ManageForeignRoutingPolicyRules=no
EOF
sudo systemctl restart systemd-networkd.service
sudo systemctl status systemd-networkd.service
```
### Wireguard Portal Service Setup
To run WireGuard Portal as a systemd service, you can create a service unit file. The easiest way to do this is by using `systemctl edit`: To run WireGuard Portal as a systemd service, you can create a service unit file. The easiest way to do this is by using `systemctl edit`:
```shell ```shell

View File

@@ -51,15 +51,6 @@ To add OIDC or OAuth2 authentication to WireGuard Portal, create a Client-ID and
configure a new authentication provider in the [`auth`](../configuration/overview.md#auth) section of the configuration file. 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). Make sure that each configured provider has a unique `provider_name` property set. Samples can be seen [here](../configuration/examples.md).
When registering the OAuth2 or OIDC application with your provider, configure the callback/redirect URL as follows:
```text
<external_url>/api/v0/auth/login/<provider_name>/callback
```
Replace `<external_url>` with the value configured in [`external_url`](../configuration/overview.md#external_url) and
`<provider_name>` with the exact `provider_name` from the matching OAuth2 or OIDC provider configuration.
#### Limiting Login to Specific Domains #### Limiting Login to Specific Domains
You can limit the login to specific domains by setting the `allowed_domains` property for OAuth2 or OIDC providers. You can limit the login to specific domains by setting the `allowed_domains` property for OAuth2 or OIDC providers.

View File

@@ -61,7 +61,7 @@ backend:
> :warning: The pfSense backend is currently **alpha**. Only basic interface and peer CRUD are supported. Traffic statistics (rx/tx, last handshake) are not exposed by the pfSense REST API and will show as empty. > :warning: The pfSense backend is currently **alpha**. Only basic interface and peer CRUD are supported. Traffic statistics (rx/tx, last handshake) are not exposed by the pfSense REST API and will show as empty.
The pfSense backend talks to the pfSense REST API (pfSense Plus / CE with the REST API package installed). Point the backend at the appliance hostname without appending `/api/v2` — the portal appends `/api/v2` automatically. wg-portal is developed for and tested against REST API v2.8.0. The pfSense backend talks to the pfSense REST API (pfSense Plus / CE with the REST API package installed). Point the backend at the appliance hostname without appending `/api/v2` — the portal appends `/api/v2` automatically.
### Prerequisites on pfSense: ### Prerequisites on pfSense:
- pfSense with the REST API package enabled (`System -> API`) and WireGuard configured. - pfSense with the REST API package enabled (`System -> API`) and WireGuard configured.

View File

@@ -3,8 +3,8 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build-dev": "vite build --mode development", "build-dev": "vite build --mode development --base=/app/",
"build": "vite build", "build": "vite build --base=/app/",
"preview": "vite preview --port 5050" "preview": "vite preview --port 5050"
}, },
"dependencies": { "dependencies": {

View File

@@ -6,7 +6,6 @@ import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { freshInterface, freshPeer, freshStats } from '@/helpers/models'; import { freshInterface, freshPeer, freshStats } from '@/helpers/models';
import Prism from "vue-prism-component"; import Prism from "vue-prism-component";
import 'prismjs/components/prism-ini';
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import { settingsStore } from "@/stores/settings"; import { settingsStore } from "@/stores/settings";
import { profileStore } from "@/stores/profile"; import { profileStore } from "@/stores/profile";

View File

@@ -5,13 +5,11 @@ import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: './',
plugins: [vue()], plugins: [vue()],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))
}, }
dedupe: ['prismjs']
}, },
build: { build: {
// //

18
go.mod
View File

@@ -9,8 +9,8 @@ require (
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-pkgz/routegroup v1.6.0 github.com/go-pkgz/routegroup v1.6.0
github.com/go-playground/validator/v10 v10.30.3 github.com/go-playground/validator/v10 v10.30.2
github.com/go-webauthn/webauthn v0.17.4 github.com/go-webauthn/webauthn v0.17.3
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/prometheus-community/pro-bing v0.8.0 github.com/prometheus-community/pro-bing v0.8.0
@@ -22,17 +22,17 @@ require (
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.53.0 golang.org/x/crypto v0.51.0
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.46.0 golang.org/x/sys v0.44.0
golang.org/x/text v0.38.0 golang.org/x/text v0.37.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.6.0 gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlserver v1.6.3 gorm.io/driver/sqlserver v1.6.3
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
pgregory.net/rapid v1.3.0 pgregory.net/rapid v1.2.0
) )
require ( require (
@@ -63,7 +63,7 @@ require (
github.com/go-sql-driver/mysql v1.10.0 // indirect github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/go-test/deep v1.1.1 // indirect github.com/go-test/deep v1.1.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.6 // indirect github.com/go-webauthn/x v0.2.5 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // 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
@@ -98,8 +98,8 @@ require (
go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.36.0 // indirect golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.55.0 // indirect golang.org/x/net v0.54.0 // indirect
golang.org/x/sync v0.21.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/tools v0.45.0 // indirect golang.org/x/tools v0.45.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect

87
go.sum
View File

@@ -3,8 +3,9 @@ filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q
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.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
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.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
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/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
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.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
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/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
@@ -12,14 +13,17 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc
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.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
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/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=
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/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
@@ -48,6 +52,8 @@ 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/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
@@ -62,6 +68,8 @@ github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
github.com/go-openapi/jsonpointer v0.23.0 h1:c25HFTJ6uWGmoe5BQI6p72p4o7KnlWYsy1MeFlAumsw=
github.com/go-openapi/jsonpointer v0.23.0/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
@@ -97,18 +105,24 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
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.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8= github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc= github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
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/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk= github.com/go-webauthn/webauthn v0.16.4 h1:R9jqR/cYZa7hRquFF7Za/8qoH/K/TIs1/Q/4CyGN+1Q=
github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8= github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4=
github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk= github.com/go-webauthn/webauthn v0.17.3 h1:XHZ0TXV7k8vChcE4TFgPitOPJ5cb7h1dpAeFDS0cjCo=
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo= github.com/go-webauthn/webauthn v0.17.3/go.mod h1:PlkMgmuL9McCT7dvgBj/Sz/fgs3V6ZID6/KnFkEcPvQ=
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
github.com/go-webauthn/x v0.2.5 h1:wEVTfU04XFyPTXGQbKOQwMKhcDWfDAkdsDDBsDaG9yY=
github.com/go-webauthn/x v0.2.5/go.mod h1:Qna/yJz9rV6lRzwl5BfYbmTJpVGxcBIds3gJtw2tlGg=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
@@ -143,6 +157,8 @@ 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.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
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=
@@ -176,15 +192,21 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mdlayher/genetlink v1.4.0 h1:f/Xs7Y2T+GyX9b3dbiUhnLE9InGs5F9RxJ2JwBMl71o= github.com/mdlayher/genetlink v1.4.0 h1:f/Xs7Y2T+GyX9b3dbiUhnLE9InGs5F9RxJ2JwBMl71o=
github.com/mdlayher/genetlink v1.4.0/go.mod h1:d1hrKr8fwZU2JkcAtQUAzeTrI7nbgQSl+5k1cC0biSA= github.com/mdlayher/genetlink v1.4.0/go.mod h1:d1hrKr8fwZU2JkcAtQUAzeTrI7nbgQSl+5k1cC0biSA=
github.com/mdlayher/netlink v1.11.0 h1:Cot7ixQZL6P/pxRFB4z3jRdGPYeZosFT+WHS3sMXy8Y=
github.com/mdlayher/netlink v1.11.0/go.mod h1:rMwDzh42W85uW3yTtiTRZFX9uway98aDQ5i+D8Jq4g4=
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI= github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA= github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo= github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/microsoft/go-mssqldb v1.9.8 h1:d4IFMvF/o+HdpXUqbBfzHvn/NlFA75YGcfHUUvDFJEM=
github.com/microsoft/go-mssqldb v1.9.8/go.mod h1:eGSRSGAW4hKMy5YcAenhCDjIRm2rhqIdmmwgciMzLus=
github.com/microsoft/go-mssqldb v1.10.0 h1:pHEt+Qz6YFPWqREq10mqSE524QQo+/QremwTCQht7TY= github.com/microsoft/go-mssqldb v1.10.0 h1:pHEt+Qz6YFPWqREq10mqSE524QQo+/QremwTCQht7TY=
github.com/microsoft/go-mssqldb v1.10.0/go.mod h1:mnG7lGa9iYJbzJqGCXyuQCegStKMr3kogDLD6+bmggg= github.com/microsoft/go-mssqldb v1.10.0/go.mod h1:mnG7lGa9iYJbzJqGCXyuQCegStKMr3kogDLD6+bmggg=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
@@ -278,14 +300,18 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
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.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.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.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
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=
@@ -305,8 +331,10 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
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.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
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=
@@ -316,8 +344,8 @@ 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.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -338,8 +366,10 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.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.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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=
@@ -368,14 +398,18 @@ 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.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
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=
@@ -404,10 +438,12 @@ gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQ
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
@@ -416,23 +452,28 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
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.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
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.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/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.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c=
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w= modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
pgregory.net/rapid v1.3.0 h1:vBvO0VSqti75J1jjYqpgPNBLKMd1+gxa9fYo7vk/Exc= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.3.0/go.mod h1:dPlE4OBBxgXPqkP79flB6sJL1dx5azpI7HQ9MY9Z7uk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@@ -140,7 +140,7 @@ func (c *PfsenseController) GetInterface(ctx context.Context, id domain.Interfac
} }
tunnelId := wgReply.Data[0].GetString("id") tunnelId := wgReply.Data[0].GetString("id")
// Query the specific tunnel endpoint to get full details including addresses // Query the specific tunnel endpoint to get full details including addresses
// Endpoint: GET /api/v2/vpn/wireguard/tunnel?id={id} // Endpoint: GET /api/v2/vpn/wireguard/tunnel?id={id}
if tunnelId != "" { if tunnelId != "" {
@@ -227,7 +227,7 @@ func (c *PfsenseController) extractAddresses(
if addrObj, ok := addrItem.(map[string]any); ok { if addrObj, ok := addrItem.(map[string]any); ok {
address := "" address := ""
mask := 0 mask := 0
// Extract address // Extract address
if addrVal, ok := addrObj["address"]; ok { if addrVal, ok := addrObj["address"]; ok {
if addrStr, ok := addrVal.(string); ok { if addrStr, ok := addrVal.(string); ok {
@@ -236,7 +236,7 @@ func (c *PfsenseController) extractAddresses(
address = fmt.Sprintf("%v", addrVal) address = fmt.Sprintf("%v", addrVal)
} }
} }
// Extract mask // Extract mask
if maskVal, ok := addrObj["mask"]; ok { if maskVal, ok := addrObj["mask"]; ok {
if maskInt, ok := maskVal.(int); ok { if maskInt, ok := maskVal.(int); ok {
@@ -249,7 +249,7 @@ func (c *PfsenseController) extractAddresses(
} }
} }
} }
// Convert to CIDR format // Convert to CIDR format
if address != "" && mask > 0 { if address != "" && mask > 0 {
cidrStr := fmt.Sprintf("%s/%d", address, mask) cidrStr := fmt.Sprintf("%s/%d", address, mask)
@@ -286,11 +286,11 @@ func (c *PfsenseController) extractAddresses(
// Each object has "address" and "mask" fields (similar to allowedips structure) // Each object has "address" and "mask" fields (similar to allowedips structure)
func (c *PfsenseController) parseAddressArray(addressArray []lowlevel.GenericJsonObject) []domain.Cidr { func (c *PfsenseController) parseAddressArray(addressArray []lowlevel.GenericJsonObject) []domain.Cidr {
addresses := make([]domain.Cidr, 0, len(addressArray)) addresses := make([]domain.Cidr, 0, len(addressArray))
for _, addrObj := range addressArray { for _, addrObj := range addressArray {
address := addrObj.GetString("address") address := addrObj.GetString("address")
mask := addrObj.GetInt("mask") mask := addrObj.GetInt("mask")
if address != "" && mask > 0 { if address != "" && mask > 0 {
cidrStr := fmt.Sprintf("%s/%d", address, mask) cidrStr := fmt.Sprintf("%s/%d", address, mask)
if cidr, err := domain.CidrFromString(cidrStr); err == nil { if cidr, err := domain.CidrFromString(cidrStr); err == nil {
@@ -303,7 +303,7 @@ func (c *PfsenseController) parseAddressArray(addressArray []lowlevel.GenericJso
} }
} }
} }
return addresses return addresses
} }
@@ -405,7 +405,7 @@ func (c *PfsenseController) GetPeers(ctx context.Context, deviceId domain.Interf
peerTun = peer.GetString("tunnel") peerTun = peer.GetString("tunnel")
} }
} }
// Only include peers that match the requested interface name // Only include peers that match the requested interface name
if peerTun != string(deviceId) { if peerTun != string(deviceId) {
if c.cfg.Debug { if c.cfg.Debug {
@@ -425,7 +425,7 @@ func (c *PfsenseController) GetPeers(ctx context.Context, deviceId domain.Interf
} }
peers = append(peers, peerModel) peers = append(peers, peerModel)
} }
if c.cfg.Debug { if c.cfg.Debug {
slog.Debug("filtered peers for interface", slog.Debug("filtered peers for interface",
"interface", deviceId, "interface", deviceId,
@@ -465,7 +465,7 @@ func (c *PfsenseController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
if itemObj, ok := item.(map[string]any); ok { if itemObj, ok := item.(map[string]any); ok {
address := "" address := ""
mask := 0 mask := 0
// Extract address // Extract address
if addrVal, ok := itemObj["address"]; ok { if addrVal, ok := itemObj["address"]; ok {
if addrStr, ok := addrVal.(string); ok { if addrStr, ok := addrVal.(string); ok {
@@ -474,7 +474,7 @@ func (c *PfsenseController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
address = fmt.Sprintf("%v", addrVal) address = fmt.Sprintf("%v", addrVal)
} }
} }
// Extract mask // Extract mask
if maskVal, ok := itemObj["mask"]; ok { if maskVal, ok := itemObj["mask"]; ok {
if maskInt, ok := maskVal.(int); ok { if maskInt, ok := maskVal.(int); ok {
@@ -487,7 +487,7 @@ func (c *PfsenseController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
} }
} }
} }
// Convert to CIDR format (e.g., "10.1.2.3/32") // Convert to CIDR format (e.g., "10.1.2.3/32")
if address != "" && mask > 0 { if address != "" && mask > 0 {
cidrStr := fmt.Sprintf("%s/%d", address, mask) cidrStr := fmt.Sprintf("%s/%d", address, mask)
@@ -502,7 +502,7 @@ func (c *PfsenseController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
allowedAddresses, _ = domain.CidrsFromString(allowedIPsStr) allowedAddresses, _ = domain.CidrsFromString(allowedIPsStr)
} }
} }
// Fallback to string parsing if array parsing didn't work // Fallback to string parsing if array parsing didn't work
if len(allowedAddresses) == 0 { if len(allowedAddresses) == 0 {
allowedIPsStr := peer.GetString("allowedips") allowedIPsStr := peer.GetString("allowedips")
@@ -516,7 +516,7 @@ func (c *PfsenseController) convertWireGuardPeer(peer lowlevel.GenericJsonObject
endpoint := peer.GetString("endpoint") endpoint := peer.GetString("endpoint")
port := peer.GetString("port") port := peer.GetString("port")
// Combine endpoint and port if both are available // Combine endpoint and port if both are available
if endpoint != "" && port != "" { if endpoint != "" && port != "" {
// Check if endpoint already contains a port // Check if endpoint already contains a port
@@ -617,22 +617,18 @@ func (c *PfsenseController) SaveInterface(
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
physicalInterface, err := c.getInterface(ctx, id) physicalInterface, err := c.getOrCreateInterface(ctx, id)
if err != nil { if err != nil {
return err return err
} }
if physicalInterface == nil { deviceId := ""
physicalInterface = &domain.PhysicalInterface{ if physicalInterface.GetExtras() != nil {
Identifier: id, if extras, ok := physicalInterface.GetExtras().(domain.PfsenseInterfaceExtras); ok {
ImportSource: domain.ControllerTypePfsense, deviceId = extras.Id
DeviceType: domain.ControllerTypePfsense,
} }
physicalInterface.SetExtras(domain.PfsenseInterfaceExtras{})
} }
deviceId := physicalInterface.GetExtras().(domain.PfsenseInterfaceExtras).Id
if updateFunc != nil { if updateFunc != nil {
physicalInterface, err = updateFunc(physicalInterface) physicalInterface, err = updateFunc(physicalInterface)
if err != nil { if err != nil {
@@ -647,14 +643,14 @@ func (c *PfsenseController) SaveInterface(
} }
} }
if err := c.createOrUpdateInterface(ctx, physicalInterface); err != nil { if err := c.updateInterface(ctx, physicalInterface); err != nil {
return err return err
} }
return nil return nil
} }
func (c *PfsenseController) getInterface( func (c *PfsenseController) getOrCreateInterface(
ctx context.Context, ctx context.Context,
id domain.InterfaceIdentifier, id domain.InterfaceIdentifier,
) (*domain.PhysicalInterface, error) { ) (*domain.PhysicalInterface, error) {
@@ -663,84 +659,50 @@ func (c *PfsenseController) getInterface(
"name": string(id), "name": string(id),
}, },
}) })
if wgReply.Status != lowlevel.PfsenseApiStatusOk { if wgReply.Status == lowlevel.PfsenseApiStatusOk && len(wgReply.Data) > 0 {
return nil, fmt.Errorf("failed to query interface %s: %v", id, wgReply.Error) return c.loadInterfaceData(ctx, wgReply.Data[0])
} }
if len(wgReply.Data) == 0 {
return nil, nil // create a new tunnel if it does not exist
// Actual endpoint: POST /api/v2/vpn/wireguard/tunnel (singular)
createReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/tunnel", lowlevel.GenericJsonObject{
"name": string(id),
})
if createReply.Status == lowlevel.PfsenseApiStatusOk {
return c.loadInterfaceData(ctx, createReply.Data)
} }
return c.loadInterfaceData(ctx, wgReply.Data[0])
return nil, fmt.Errorf("failed to create interface %s: %v", id, createReply.Error)
} }
type pfsenseWireGuardAddress struct { func (c *PfsenseController) updateInterface(ctx context.Context, pi *domain.PhysicalInterface) error {
Address string `json:"address"`
Mask int `json:"mask"`
Descr string `json:"descr"`
}
func cidrToPfsense(cidr *domain.Cidr) pfsenseWireGuardAddress {
return pfsenseWireGuardAddress{
Address: cidr.Addr,
Mask: cidr.NetLength,
// supported in pfsense, but not in wg-portal GUI
Descr: "",
}
}
func (c *PfsenseController) createOrUpdateInterface(ctx context.Context, pi *domain.PhysicalInterface) error {
extras := pi.GetExtras().(domain.PfsenseInterfaceExtras) extras := pi.GetExtras().(domain.PfsenseInterfaceExtras)
interfaceId := extras.Id interfaceId := extras.Id
payload := lowlevel.GenericJsonObject{ payload := lowlevel.GenericJsonObject{
"name": string(pi.Identifier), "name": string(pi.Identifier),
"descr": extras.Comment, "description": extras.Comment,
"mtu": pi.Mtu, "mtu": strconv.Itoa(pi.Mtu),
"listenport": strconv.Itoa(pi.ListenPort), "listenport": strconv.Itoa(pi.ListenPort),
"privatekey": pi.KeyPair.PrivateKey, "privatekey": pi.KeyPair.PrivateKey,
"disabled": strconv.FormatBool(!pi.DeviceUp), "disabled": strconv.FormatBool(!pi.DeviceUp),
} }
addresses := make([]pfsenseWireGuardAddress, 0, len(pi.Addresses)) // Add addresses if present
for _, addr := range pi.Addresses { if len(pi.Addresses) > 0 {
addresses = append(addresses, cidrToPfsense(&addr)) addresses := make([]string, 0, len(pi.Addresses))
} for _, addr := range pi.Addresses {
payload["addresses"] = addresses addresses = append(addresses, addr.String())
if interfaceId == "" {
// Actual endpoint: POST /api/v2/vpn/wireguard/tunnel (singular)
createReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/tunnel", payload)
if createReply.Status != lowlevel.PfsenseApiStatusOk {
return fmt.Errorf("failed to create interface %s: %v", pi.Identifier, createReply.Error)
} }
// Capture the newly-assigned ID so callers see it payload["addresses"] = strings.Join(addresses, ",")
if newId := createReply.Data.GetString("id"); newId != "" {
extras.Id = newId
pi.SetExtras(extras)
}
if applyReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/apply", lowlevel.GenericJsonObject{}); applyReply.Status != lowlevel.PfsenseApiStatusOk {
return fmt.Errorf("failed to apply WireGuard changes after creating interface %s: %v",
pi.Identifier, applyReply.Error)
}
return nil
} }
interfaceIdInt, err := strconv.Atoi(interfaceId) // Actual endpoint: PATCH /api/v2/vpn/wireguard/tunnel?id={id}
if err != nil { wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/tunnel?id="+interfaceId, payload)
return fmt.Errorf("invalid pfSense interface id %q for %s: %w", interfaceId, pi.Identifier, err)
}
payload["id"] = interfaceIdInt
// Actual endpoint: PATCH /api/v2/vpn/wireguard/tunnel
wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/tunnel", payload)
if wgReply.Status != lowlevel.PfsenseApiStatusOk { if wgReply.Status != lowlevel.PfsenseApiStatusOk {
return fmt.Errorf("failed to update interface %s: %v", pi.Identifier, wgReply.Error) return fmt.Errorf("failed to update interface %s: %v", pi.Identifier, wgReply.Error)
} }
if applyReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/apply", lowlevel.GenericJsonObject{}); applyReply.Status != lowlevel.PfsenseApiStatusOk {
return fmt.Errorf("failed to apply WireGuard changes after updating interface %s: %v",
pi.Identifier, applyReply.Error)
}
return nil return nil
} }
@@ -764,10 +726,8 @@ func (c *PfsenseController) DeleteInterface(ctx context.Context, id domain.Inter
} }
interfaceId := wgReply.Data[0].GetString("id") interfaceId := wgReply.Data[0].GetString("id")
// Actual endpoint: DELETE /api/v2/vpn/wireguard/tunnel?id={id}&apply=true // Actual endpoint: DELETE /api/v2/vpn/wireguard/tunnel?id={id}
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/tunnel", &lowlevel.PfsenseRequestOptions{ deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/tunnel?id="+interfaceId)
Filters: map[string]string{"id": interfaceId, "apply": "true"},
})
if deleteReply.Status != lowlevel.PfsenseApiStatusOk { if deleteReply.Status != lowlevel.PfsenseApiStatusOk {
return fmt.Errorf("failed to delete WireGuard interface %s: %v", id, deleteReply.Error) return fmt.Errorf("failed to delete WireGuard interface %s: %v", id, deleteReply.Error)
} }
@@ -786,22 +746,18 @@ func (c *PfsenseController) SavePeer(
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
physicalPeer, err := c.getPeer(ctx, deviceId, id) physicalPeer, err := c.getOrCreatePeer(ctx, deviceId, id)
if err != nil { if err != nil {
return err return err
} }
if physicalPeer == nil { peerId := ""
physicalPeer = &domain.PhysicalPeer{ if physicalPeer.GetExtras() != nil {
Identifier: id, if extras, ok := physicalPeer.GetExtras().(domain.PfsensePeerExtras); ok {
KeyPair: domain.KeyPair{PublicKey: string(id)}, peerId = extras.Id
ImportSource: domain.ControllerTypePfsense,
} }
physicalPeer.SetExtras(domain.PfsensePeerExtras{})
} }
peerId := physicalPeer.GetExtras().(domain.PfsensePeerExtras).Id
physicalPeer, err = updateFunc(physicalPeer) physicalPeer, err = updateFunc(physicalPeer)
if err != nil { if err != nil {
return err return err
@@ -814,14 +770,14 @@ func (c *PfsenseController) SavePeer(
} }
} }
if err := c.createOrUpdatePeer(ctx, deviceId, physicalPeer); err != nil { if err := c.updatePeer(ctx, deviceId, physicalPeer); err != nil {
return err return err
} }
return nil return nil
} }
func (c *PfsenseController) getPeer( func (c *PfsenseController) getOrCreatePeer(
ctx context.Context, ctx context.Context,
deviceId domain.InterfaceIdentifier, deviceId domain.InterfaceIdentifier,
id domain.PeerIdentifier, id domain.PeerIdentifier,
@@ -831,25 +787,40 @@ func (c *PfsenseController) getPeer(
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/peers", &lowlevel.PfsenseRequestOptions{ wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/peers", &lowlevel.PfsenseRequestOptions{
Filters: map[string]string{ Filters: map[string]string{
"publickey": string(id), "publickey": string(id),
"tun": string(deviceId), // Use "tun" field name as that's what the API uses "tun": string(deviceId), // Use "tun" field name as that's what the API uses
}, },
}) })
if wgReply.Status != lowlevel.PfsenseApiStatusOk { if wgReply.Status == lowlevel.PfsenseApiStatusOk && len(wgReply.Data) > 0 {
return nil, fmt.Errorf("failed to query peer %s for interface %s: %v", id, deviceId, wgReply.Error) slog.Debug("found existing pfSense peer", "peer", id, "interface", deviceId)
} existingPeer, err := c.convertWireGuardPeer(wgReply.Data[0])
if len(wgReply.Data) == 0 { if err != nil {
return nil, nil return nil, err
}
return &existingPeer, nil
} }
slog.Debug("found existing pfSense peer", "peer", id, "interface", deviceId) // create a new peer if it does not exist
existingPeer, err := c.convertWireGuardPeer(wgReply.Data[0]) // Actual endpoint: POST /api/v2/vpn/wireguard/peer (singular)
if err != nil { slog.Debug("creating new pfSense peer", "peer", id, "interface", deviceId)
return nil, err createReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/peer", lowlevel.GenericJsonObject{
"name": fmt.Sprintf("wg-%s", id[0:8]),
"interface": string(deviceId),
"publickey": string(id),
"allowedips": "0.0.0.0/0", // Use 0.0.0.0/0 as default, will be updated by updatePeer
})
if createReply.Status == lowlevel.PfsenseApiStatusOk {
newPeer, err := c.convertWireGuardPeer(createReply.Data)
if err != nil {
return nil, err
}
slog.Debug("successfully created pfSense peer", "peer", id, "interface", deviceId)
return &newPeer, nil
} }
return &existingPeer, nil
return nil, fmt.Errorf("failed to create peer %s for interface %s: %v", id, deviceId, createReply.Error)
} }
func (c *PfsenseController) createOrUpdatePeer( func (c *PfsenseController) updatePeer(
ctx context.Context, ctx context.Context,
deviceId domain.InterfaceIdentifier, deviceId domain.InterfaceIdentifier,
pp *domain.PhysicalPeer, pp *domain.PhysicalPeer,
@@ -857,74 +828,36 @@ func (c *PfsenseController) createOrUpdatePeer(
extras := pp.GetExtras().(domain.PfsensePeerExtras) extras := pp.GetExtras().(domain.PfsensePeerExtras)
peerId := extras.Id peerId := extras.Id
allowedIPsStr := domain.CidrsToString(pp.AllowedIPs)
slog.Debug("updating pfSense peer",
"peer", pp.Identifier,
"interface", deviceId,
"allowed-ips", allowedIPsStr,
"allowed-ips-count", len(pp.AllowedIPs),
"disabled", extras.Disabled)
payload := lowlevel.GenericJsonObject{ payload := lowlevel.GenericJsonObject{
"tun": string(deviceId), "name": extras.Name,
"descr": extras.Name, "description": extras.Comment,
"presharedkey": string(pp.PresharedKey), "presharedkey": string(pp.PresharedKey),
"publickey": pp.KeyPair.PublicKey, "publickey": pp.KeyPair.PublicKey,
"persistentkeepalive": pp.PersistentKeepalive, "privatekey": pp.KeyPair.PrivateKey,
"enabled": !extras.Disabled, "persistentkeepalive": strconv.Itoa(pp.PersistentKeepalive),
"disabled": strconv.FormatBool(extras.Disabled),
"allowedips": allowedIPsStr,
} }
if pp.Endpoint != "" { if pp.Endpoint != "" {
payload["endpoint"] = pp.Endpoint payload["endpoint"] = pp.Endpoint
} }
allowedIps := make([]pfsenseWireGuardAddress, 0, len(pp.AllowedIPs)) // Actual endpoint: PATCH /api/v2/vpn/wireguard/peer?id={id}
for _, addr := range pp.AllowedIPs { wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/peer?id="+peerId, payload)
allowedIps = append(allowedIps, cidrToPfsense(&addr))
}
payload["allowedips"] = allowedIps
if peerId == "" {
slog.Debug("creating new pfSense peer",
"peer", pp.Identifier,
"interface", deviceId,
"allowed-ips", domain.CidrsToString(pp.AllowedIPs),
"disabled", extras.Disabled)
// Actual endpoint: POST /api/v2/vpn/wireguard/peer (singular)
createReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/peer", payload)
if createReply.Status != lowlevel.PfsenseApiStatusOk {
return fmt.Errorf("failed to create peer %s for interface %s: %v",
pp.Identifier, deviceId, createReply.Error)
}
if newId := createReply.Data.GetString("id"); newId != "" {
extras.Id = newId
pp.SetExtras(extras)
}
if applyReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/apply", lowlevel.GenericJsonObject{}); applyReply.Status != lowlevel.PfsenseApiStatusOk {
return fmt.Errorf("failed to apply WireGuard changes after creating peer %s on interface %s: %v",
pp.Identifier, deviceId, applyReply.Error)
}
slog.Debug("successfully created pfSense peer", "peer", pp.Identifier, "interface", deviceId)
return nil
}
slog.Debug("updating pfSense peer",
"peer", pp.Identifier,
"interface", deviceId,
"allowed-ips", domain.CidrsToString(pp.AllowedIPs),
"allowed-ips-count", len(pp.AllowedIPs),
"disabled", extras.Disabled)
peerIdInt, err := strconv.Atoi(peerId)
if err != nil {
return fmt.Errorf("invalid pfSense peer id %q for %s: %w", peerId, pp.Identifier, err)
}
payload["id"] = peerIdInt
// Actual endpoint: PATCH /api/v2/vpn/wireguard/peer
wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/peer", payload)
if wgReply.Status != lowlevel.PfsenseApiStatusOk { if wgReply.Status != lowlevel.PfsenseApiStatusOk {
return fmt.Errorf("failed to update peer %s on interface %s: %v", pp.Identifier, deviceId, wgReply.Error) return fmt.Errorf("failed to update peer %s on interface %s: %v", pp.Identifier, deviceId, wgReply.Error)
} }
if applyReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/apply", lowlevel.GenericJsonObject{}); applyReply.Status != lowlevel.PfsenseApiStatusOk {
return fmt.Errorf("failed to apply WireGuard changes after updating peer %s on interface %s: %v",
pp.Identifier, deviceId, applyReply.Error)
}
if extras.Disabled { if extras.Disabled {
slog.Debug("successfully disabled pfSense peer", "peer", pp.Identifier, "interface", deviceId) slog.Debug("successfully disabled pfSense peer", "peer", pp.Identifier, "interface", deviceId)
} else { } else {
@@ -949,7 +882,7 @@ func (c *PfsenseController) DeletePeer(
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/peers", &lowlevel.PfsenseRequestOptions{ wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/peers", &lowlevel.PfsenseRequestOptions{
Filters: map[string]string{ Filters: map[string]string{
"publickey": string(id), "publickey": string(id),
"tun": string(deviceId), // Use "tun" field name as that's what the API uses "tun": string(deviceId), // Use "tun" field name as that's what the API uses
}, },
}) })
if wgReply.Status != lowlevel.PfsenseApiStatusOk { if wgReply.Status != lowlevel.PfsenseApiStatusOk {
@@ -960,10 +893,8 @@ func (c *PfsenseController) DeletePeer(
} }
peerId := wgReply.Data[0].GetString("id") peerId := wgReply.Data[0].GetString("id")
// Actual endpoint: DELETE /api/v2/vpn/wireguard/peer?id={id}&apply=true // Actual endpoint: DELETE /api/v2/vpn/wireguard/peer?id={id}
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/peer", &lowlevel.PfsenseRequestOptions{ deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/peer?id="+peerId)
Filters: map[string]string{"id": peerId, "apply": "true"},
})
if deleteReply.Status != lowlevel.PfsenseApiStatusOk { if deleteReply.Status != lowlevel.PfsenseApiStatusOk {
return fmt.Errorf("failed to delete WireGuard peer %s for interface %s: %v", id, deviceId, deleteReply.Error) return fmt.Errorf("failed to delete WireGuard peer %s for interface %s: %v", id, deviceId, deleteReply.Error)
} }
@@ -1045,3 +976,4 @@ func (c *PfsenseController) PingAddresses(
} }
// endregion statistics-related // endregion statistics-related

View File

@@ -283,13 +283,10 @@ func (s *Server) updateBasePathInFrontend(useEmbeddedFrontend bool) ([]byte, []b
} else { } else {
customIndexFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "index.html")) customIndexFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "index.html"))
} }
// rewrite relative paths newIndexStr := strings.ReplaceAll(string(customIndexFile), "src=\"/", "src=\""+s.cfg.Web.BasePath+"/")
newIndexStr := strings.ReplaceAll(string(customIndexFile), "src=\"./", "src=\""+s.cfg.Web.BasePath+"/app/") newIndexStr = strings.ReplaceAll(newIndexStr, "href=\"/", "href=\""+s.cfg.Web.BasePath+"/")
newIndexStr = strings.ReplaceAll(newIndexStr, "href=\"./", "href=\""+s.cfg.Web.BasePath+"/app/")
// rewrite absolute paths (api calls)
newIndexStr = strings.ReplaceAll(newIndexStr, "src=\"/api/", "src=\""+s.cfg.Web.BasePath+"/api/")
re := regexp.MustCompile(`assets/(index-.+.css)`) re := regexp.MustCompile(`/app/assets/(index-.+.css)`)
match := re.FindStringSubmatch(newIndexStr) match := re.FindStringSubmatch(newIndexStr)
cssFileName := match[1] cssFileName := match[1]
@@ -299,7 +296,7 @@ func (s *Server) updateBasePathInFrontend(useEmbeddedFrontend bool) ([]byte, []b
} else { } else {
customCssFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "/assets/", cssFileName)) customCssFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "/assets/", cssFileName))
} }
newCssStr := strings.ReplaceAll(string(customCssFile), "./assets/", s.cfg.Web.BasePath+"/app/assets/") newCssStr := strings.ReplaceAll(string(customCssFile), "/app/assets/", s.cfg.Web.BasePath+"/app/assets/")
return []byte(newIndexStr), []byte(newCssStr), cssFileName return []byte(newIndexStr), []byte(newCssStr), cssFileName
} }

View File

@@ -3,10 +3,8 @@ package handlers
import ( import (
"context" "context"
"net/http" "net/http"
"net/url"
"strings" "strings"
"sync" "sync"
"time"
"github.com/go-pkgz/routegroup" "github.com/go-pkgz/routegroup"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@@ -16,83 +14,49 @@ import (
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
) )
const (
websocketPeerUserIdentifierCacheTTL = 90 * time.Second
websocketPeerUserIdentifierCacheCleanupInterval = websocketPeerUserIdentifierCacheTTL * 2
)
type WebsocketEventBus interface { type WebsocketEventBus interface {
Subscribe(topic string, fn any) error Subscribe(topic string, fn any) error
Unsubscribe(topic string, fn any) error Unsubscribe(topic string, fn any) error
} }
type WebsocketPeerService interface {
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
}
type WebsocketEndpoint struct { type WebsocketEndpoint struct {
authenticator Authenticator authenticator Authenticator
bus WebsocketEventBus bus WebsocketEventBus
peerService WebsocketPeerService
upgrader websocket.Upgrader upgrader websocket.Upgrader
ownershipCache map[domain.PeerIdentifier]peerUserIdentifierCacheEntry
ownershipCacheMux sync.Mutex
} }
func NewWebsocketEndpoint( func NewWebsocketEndpoint(cfg *config.Config, auth Authenticator, bus WebsocketEventBus) *WebsocketEndpoint {
cfg *config.Config,
auth Authenticator,
bus WebsocketEventBus,
peerService WebsocketPeerService,
) *WebsocketEndpoint {
return &WebsocketEndpoint{ return &WebsocketEndpoint{
authenticator: auth, authenticator: auth,
bus: bus, bus: bus,
peerService: peerService,
upgrader: websocket.Upgrader{ upgrader: websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 1024, WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { CheckOrigin: func(r *http.Request) bool {
return matchOrigin(cfg.Web.ExternalUrl, r.Header.Get("Origin")) origin := r.Header.Get("Origin")
return strings.HasPrefix(origin, cfg.Web.ExternalUrl)
}, },
}, },
ownershipCache: make(map[domain.PeerIdentifier]peerUserIdentifierCacheEntry),
ownershipCacheMux: sync.Mutex{},
} }
} }
func (e *WebsocketEndpoint) GetName() string { func (e WebsocketEndpoint) GetName() string {
return "WebsocketEndpoint" return "WebsocketEndpoint"
} }
func (e *WebsocketEndpoint) RegisterRoutes(g *routegroup.Bundle) { func (e WebsocketEndpoint) RegisterRoutes(g *routegroup.Bundle) {
g.With(e.authenticator.LoggedIn()).HandleFunc("GET /ws", e.handleWebsocket()) g.With(e.authenticator.LoggedIn()).HandleFunc("GET /ws", e.handleWebsocket())
} }
// StartBackgroundJobs starts background jobs like the expired peers check.
// This method is non-blocking.
func (e *WebsocketEndpoint) StartBackgroundJobs(ctx context.Context) {
go e.startOwnerCacheCleanup(ctx)
}
// wsMessage represents a message sent over websocket to the frontend // wsMessage represents a message sent over websocket to the frontend
type wsMessage struct { type wsMessage struct {
Type string `json:"type"` // either "peer_stats" or "interface_stats" Type string `json:"type"` // either "peer_stats" or "interface_stats"
Data any `json:"data"` // domain.TrafficDelta Data any `json:"data"` // domain.TrafficDelta
} }
// peerUserIdentifierCacheEntry is a cache entry object that reduces database load when checking peer ownership. func (e WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
type peerUserIdentifierCacheEntry struct {
userIdentifier domain.UserIdentifier
expiresAt time.Time
}
func (e *WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
userInfo := domain.GetUserInfo(r.Context())
conn, err := e.upgrader.Upgrade(w, r, nil) conn, err := e.upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
return return
@@ -110,29 +74,9 @@ func (e *WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
} }
peerStatsHandler := func(status domain.TrafficDelta) { peerStatsHandler := func(status domain.TrafficDelta) {
if !userInfo.IsAdmin {
// lookup peer user-info to validate ownership
peerUserIdentifier, err := e.getPeerUserIdentifier(ctx, domain.PeerIdentifier(status.EntityId))
if err != nil {
return
}
if peerUserIdentifier == "" {
return // if peer is not assigned to any user, dont send stats
}
if peerUserIdentifier != userInfo.Id {
return // only expose stats for own peers
}
}
_ = writeJSON(wsMessage{Type: "peer_stats", Data: status}) _ = writeJSON(wsMessage{Type: "peer_stats", Data: status})
} }
interfaceStatsHandler := func(status domain.TrafficDelta) { interfaceStatsHandler := func(status domain.TrafficDelta) {
if !userInfo.IsAdmin {
return // interface stats will only be exposed to admins
}
_ = writeJSON(wsMessage{Type: "interface_stats", Data: status}) _ = writeJSON(wsMessage{Type: "interface_stats", Data: status})
} }
@@ -154,72 +98,3 @@ func (e *WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
<-ctx.Done() <-ctx.Done()
} }
} }
func (e *WebsocketEndpoint) getPeerUserIdentifier(
ctx context.Context,
peerIdentifier domain.PeerIdentifier,
) (domain.UserIdentifier, error) {
now := time.Now()
e.ownershipCacheMux.Lock()
entry, ok := e.ownershipCache[peerIdentifier]
if ok && now.Before(entry.expiresAt) {
e.ownershipCacheMux.Unlock()
return entry.userIdentifier, nil
}
e.ownershipCacheMux.Unlock()
peer, err := e.peerService.GetPeer(ctx, peerIdentifier)
if err != nil {
return "", err
}
e.ownershipCacheMux.Lock()
defer e.ownershipCacheMux.Unlock()
e.ownershipCache[peerIdentifier] = peerUserIdentifierCacheEntry{
userIdentifier: peer.UserIdentifier,
expiresAt: now.Add(websocketPeerUserIdentifierCacheTTL),
}
return peer.UserIdentifier, nil
}
func (e *WebsocketEndpoint) startOwnerCacheCleanup(ctx context.Context) {
ticker := time.NewTicker(websocketPeerUserIdentifierCacheCleanupInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case now := <-ticker.C:
e.cleanupOwnerCache(now)
}
}
}
func (e *WebsocketEndpoint) cleanupOwnerCache(now time.Time) {
e.ownershipCacheMux.Lock()
defer e.ownershipCacheMux.Unlock()
for peerIdentifier, entry := range e.ownershipCache {
if !now.Before(entry.expiresAt) {
delete(e.ownershipCache, peerIdentifier)
}
}
}
func matchOrigin(externalBaseUrl, origin string) bool {
originURL, err := url.Parse(origin)
if err != nil {
return false
}
externalURL, err := url.Parse(externalBaseUrl)
if err != nil {
return false
}
return originURL.Scheme == externalURL.Scheme &&
strings.EqualFold(originURL.Host, externalURL.Host)
}

View File

@@ -1,249 +0,0 @@
package handlers
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gorilla/websocket"
evbus "github.com/vardius/message-bus"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
// region test-helper
type websocketTestPeerService struct {
peers map[domain.PeerIdentifier]*domain.Peer
}
func (s websocketTestPeerService) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
peer, ok := s.peers[id]
if !ok {
return nil, errors.New("peer not found")
}
return peer, nil
}
func newTestWebsocketConnection(
t *testing.T,
bus evbus.MessageBus,
userInfo *domain.ContextUserInfo,
peers map[domain.PeerIdentifier]*domain.Peer,
) (*websocket.Conn, func()) {
t.Helper()
cfg := &config.Config{}
endpoint := NewWebsocketEndpoint(cfg, nil, bus, websocketTestPeerService{peers: peers})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(domain.SetUserInfo(r.Context(), userInfo))
endpoint.handleWebsocket()(w, r)
}))
cfg.Web.ExternalUrl = server.URL
wsURL := "ws" + server.URL[len("http"):]
conn, _, err := websocket.DefaultDialer.Dial(wsURL, http.Header{"Origin": []string{server.URL}})
if err != nil {
server.Close()
t.Fatalf("failed to dial websocket: %v", err)
}
cleanup := func() {
conn.Close()
server.Close()
}
return conn, cleanup
}
func assertWebsocketMessage(t *testing.T, conn *websocket.Conn, messageType string, entityId string) {
t.Helper()
if err := conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatalf("failed to set read deadline: %v", err)
}
var message wsMessage
if err := conn.ReadJSON(&message); err != nil {
t.Fatalf("failed to read websocket message: %v", err)
}
if message.Type != messageType {
t.Fatalf("unexpected message type: got %q, want %q", message.Type, messageType)
}
data, ok := message.Data.(map[string]any)
if !ok {
t.Fatalf("unexpected message data type: %T", message.Data)
}
if data["EntityId"] != entityId {
t.Fatalf("unexpected entity id: got %v, want %q", data["EntityId"], entityId)
}
}
func assertNoWebsocketMessage(t *testing.T, conn *websocket.Conn) {
t.Helper()
if err := conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
t.Fatalf("failed to set read deadline: %v", err)
}
var message wsMessage
if err := conn.ReadJSON(&message); err == nil {
t.Fatalf("unexpected websocket message: %+v", message)
}
}
// endregion test-helper
func TestWebsocketEndpointAllowsOwnPeerStatsForNonAdmin(t *testing.T) {
bus := evbus.New(10)
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "user-a"},
map[domain.PeerIdentifier]*domain.Peer{
"own-peer": {Identifier: "own-peer", UserIdentifier: "user-a"},
})
defer cleanup()
bus.Publish(app.TopicPeerStatsUpdated, domain.TrafficDelta{EntityId: "own-peer", BytesReceivedPerSecond: 1})
assertWebsocketMessage(t, conn, "peer_stats", "own-peer")
}
func TestWebsocketEndpointCleansExpiredPeerUserIdentifierCache(t *testing.T) {
now := time.Now()
endpoint := &WebsocketEndpoint{
ownershipCache: map[domain.PeerIdentifier]peerUserIdentifierCacheEntry{
"expired-peer": {
userIdentifier: "user-a",
expiresAt: now.Add(-time.Second),
},
"active-peer": {
userIdentifier: "user-b",
expiresAt: now.Add(time.Second),
},
},
}
endpoint.cleanupOwnerCache(now)
if _, ok := endpoint.ownershipCache["expired-peer"]; ok {
t.Fatal("expired peer cache entry was not removed")
}
if _, ok := endpoint.ownershipCache["active-peer"]; !ok {
t.Fatal("active peer cache entry was removed")
}
}
func TestWebsocketEndpointFiltersOtherPeerStatsForNonAdmin(t *testing.T) {
bus := evbus.New(10)
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "user-a"},
map[domain.PeerIdentifier]*domain.Peer{
"other-peer": {Identifier: "other-peer", UserIdentifier: "user-b"},
})
defer cleanup()
bus.Publish(app.TopicPeerStatsUpdated, domain.TrafficDelta{EntityId: "other-peer", BytesReceivedPerSecond: 1})
assertNoWebsocketMessage(t, conn)
}
func TestWebsocketEndpointFiltersUnknownPeerStatsForNonAdmin(t *testing.T) {
bus := evbus.New(10)
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "user-a"},
map[domain.PeerIdentifier]*domain.Peer{
"other-peer": {Identifier: "other-peer", UserIdentifier: ""},
})
defer cleanup()
bus.Publish(app.TopicPeerStatsUpdated, domain.TrafficDelta{EntityId: "other-peer", BytesReceivedPerSecond: 1})
assertNoWebsocketMessage(t, conn)
}
func TestWebsocketEndpointFiltersUnknownPeerStatsForNonAdmin2(t *testing.T) {
bus := evbus.New(10)
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "user-a"}, nil)
defer cleanup()
bus.Publish(app.TopicPeerStatsUpdated, domain.TrafficDelta{EntityId: "unknown-peer", BytesReceivedPerSecond: 1})
assertNoWebsocketMessage(t, conn)
}
func TestWebsocketEndpointFiltersInterfaceStatsForNonAdmin(t *testing.T) {
bus := evbus.New(10)
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "user-a"}, nil)
defer cleanup()
bus.Publish(app.TopicInterfaceStatsUpdated, domain.TrafficDelta{EntityId: "wg0", BytesReceivedPerSecond: 1})
assertNoWebsocketMessage(t, conn)
}
func TestWebsocketEndpointAllowsAllStatsForAdmin(t *testing.T) {
bus := evbus.New(10)
conn, cleanup := newTestWebsocketConnection(t, bus, &domain.ContextUserInfo{Id: "admin", IsAdmin: true}, nil)
defer cleanup()
bus.Publish(app.TopicPeerStatsUpdated, domain.TrafficDelta{EntityId: "other-peer", BytesReceivedPerSecond: 1})
assertWebsocketMessage(t, conn, "peer_stats", "other-peer")
bus.Publish(app.TopicInterfaceStatsUpdated, domain.TrafficDelta{EntityId: "wg0", BytesReceivedPerSecond: 1})
assertWebsocketMessage(t, conn, "interface_stats", "wg0")
}
func Test_matchOrigin(t *testing.T) {
tests := []struct {
name string
externalBaseUrl string
origin string
want bool
}{
{
name: "matching origin",
externalBaseUrl: "https://example.com",
origin: "https://example.com",
want: true,
},
{
name: "matching origin with path",
externalBaseUrl: "https://example.com/app1",
origin: "https://example.com/app2",
want: true,
},
{
name: "non-matching origin with different host",
externalBaseUrl: "https://example.com",
origin: "https://example.com.malicious.com",
want: false,
},
{
name: "non-matching origin with different scheme",
externalBaseUrl: "https://example.com",
origin: "http://example.com",
want: false,
},
{
name: "invalid origin URL",
externalBaseUrl: "https://example.com",
origin: "://invalid-url",
want: false,
},
{
name: "invalid externalBaseUrl",
externalBaseUrl: "://invalid-url",
origin: "https://example.com",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := matchOrigin(tt.externalBaseUrl, tt.origin)
if got != tt.want {
t.Errorf("matchOrigin() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -180,7 +180,7 @@ func (o OidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oa
return o.cfg.Exchange(ctx, code, opts...) return o.cfg.Exchange(ctx, code, opts...)
} }
// GetUserInfo retrieves the user info from the token and the userinfo endpoint. // GetUserInfo retrieves the user info from the token.
func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) ( func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (
map[string]any, map[string]any,
error, error,
@@ -218,41 +218,6 @@ func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token,
return nil, fmt.Errorf("failed to parse extra claims: %w", err) return nil, fmt.Errorf("failed to parse extra claims: %w", err)
} }
// Fetch additional user information from the userinfo endpoint
userInfo, err := o.provider.UserInfo(ctx, oauth2.StaticTokenSource(token))
if err != nil {
if o.sensitiveInfoLogging {
slog.Debug("OIDC: failed to fetch user info from endpoint", "provider", o.name, "error", err)
}
// Don't fail the entire flow if userinfo endpoint is unavailable;
// ID token claims may be sufficient
slog.Debug("OIDC: proceeding with ID token claims only", "provider", o.name)
} else {
// Parse claims from userinfo endpoint response
var userInfoFields map[string]any
if err = userInfo.Claims(&userInfoFields); err != nil {
if o.sensitiveInfoLogging {
slog.Debug("OIDC: failed to parse userinfo claims", "provider", o.name, "error", err)
}
// Don't fail if we can't parse userinfo; continue with ID token claims
slog.Debug("OIDC: proceeding with ID token claims only", "provider", o.name)
} else {
// Merge userinfo fields into tokenFields, preferring ID token claims
for key, value := range userInfoFields {
if _, exists := tokenFields[key]; !exists {
tokenFields[key] = value
}
}
if o.userInfoLogging {
contents, _ := json.Marshal(userInfoFields)
slog.Debug("OIDC: user info from endpoint",
"source", o.name,
"info", string(contents))
}
}
}
if o.userInfoLogging { if o.userInfoLogging {
contents, _ := json.Marshal(tokenFields) contents, _ := json.Marshal(tokenFields)
slog.Debug("OIDC: user info debug", slog.Debug("OIDC: user info debug",

View File

@@ -893,7 +893,16 @@ func (m Manager) importInterface(
} }
} }
iface.Type = inferImportedInterfaceType(iface, peers) // try to predict the interface type based on the number of peers
switch len(peers) {
case 0:
iface.Type = domain.InterfaceTypeAny // no peers means this is an unknown interface
case 1:
iface.Type = domain.InterfaceTypeClient // one peer means this is a client interface
default: // multiple peers means this is a server interface
iface.Type = domain.InterfaceTypeServer
}
existingInterface, err := m.db.GetInterface(ctx, iface.Identifier) existingInterface, err := m.db.GetInterface(ctx, iface.Identifier)
if err != nil && !errors.Is(err, domain.ErrNotFound) { if err != nil && !errors.Is(err, domain.ErrNotFound) {
@@ -921,20 +930,6 @@ func (m Manager) importInterface(
return nil return nil
} }
func inferImportedInterfaceType(iface *domain.Interface, peers []domain.PhysicalPeer) domain.InterfaceType {
switch len(peers) {
case 0:
return domain.InterfaceTypeAny // no peers means this is an unknown interface
case 1:
if iface.ListenPort > 0 {
return domain.InterfaceTypeServer // a listening interface with one peer is commonly a site-to-site server
}
return domain.InterfaceTypeClient
default: // multiple peers means this is a server interface
return domain.InterfaceTypeServer
}
}
// extractPfsenseDefaultsFromPeers extracts common endpoint and DNS information from peers // extractPfsenseDefaultsFromPeers extracts common endpoint and DNS information from peers
// For server interfaces, peers typically have endpoints pointing to the server, so we use the most common one // For server interfaces, peers typically have endpoints pointing to the server, so we use the most common one
func extractPfsenseDefaultsFromPeers(peers []domain.PhysicalPeer, listenPort int) (endpoint, dns string) { func extractPfsenseDefaultsFromPeers(peers []domain.PhysicalPeer, listenPort int) (endpoint, dns string) {

View File

@@ -10,49 +10,6 @@ import (
"github.com/h44z/wg-portal/internal/domain" "github.com/h44z/wg-portal/internal/domain"
) )
func TestInferImportedInterfaceType(t *testing.T) {
tests := []struct {
name string
listenPort int
peerCount int
expected domain.InterfaceType
}{
{
name: "no peers stays unknown",
listenPort: 51820,
peerCount: 0,
expected: domain.InterfaceTypeAny,
},
{
name: "single peer with listen port is server",
listenPort: 51820,
peerCount: 1,
expected: domain.InterfaceTypeServer,
},
{
name: "single peer without listen port stays client",
listenPort: 0,
peerCount: 1,
expected: domain.InterfaceTypeClient,
},
{
name: "multiple peers is server",
listenPort: 0,
peerCount: 2,
expected: domain.InterfaceTypeServer,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
iface := &domain.Interface{ListenPort: tt.listenPort}
peers := make([]domain.PhysicalPeer, tt.peerCount)
assert.Equal(t, tt.expected, inferImportedInterfaceType(iface, peers))
})
}
}
func TestImportPeer_AddressMapping(t *testing.T) { func TestImportPeer_AddressMapping(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@@ -57,9 +57,6 @@ type EmptyResponse struct{}
func (JsonObject GenericJsonObject) GetString(key string) string { func (JsonObject GenericJsonObject) GetString(key string) string {
if value, ok := JsonObject[key]; ok { if value, ok := JsonObject[key]; ok {
if value == nil {
return ""
}
if strValue, ok := value.(string); ok { if strValue, ok := value.(string); ok {
return strValue return strValue
} else { } else {

View File

@@ -23,7 +23,7 @@ import (
// region models // region models
const ( const (
PfsenseApiStatusOk = "ok" // pfSense REST API uses "ok" in response PfsenseApiStatusOk = "ok" // pfSense REST API uses "ok" in response
PfsenseApiStatusError = "error" PfsenseApiStatusError = "error"
) )
@@ -37,8 +37,8 @@ const (
type PfsenseApiResponse[T any] struct { type PfsenseApiResponse[T any] struct {
Status string Status string
Code int Code int
Data T `json:"data,omitempty"` Data T `json:"data,omitempty"`
Error *PfsenseApiError `json:"error,omitempty"` Error *PfsenseApiError `json:"error,omitempty"`
} }
type PfsenseApiError struct { type PfsenseApiError struct {
@@ -193,7 +193,6 @@ func (p *PfsenseApiClient) preparePayloadRequest(
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to marshal payload: %w", err) return nil, fmt.Errorf("failed to marshal payload: %w", err)
} }
p.debugLog("Prepared payload", "payload", string(payloadBytes))
req, err := http.NewRequestWithContext(ctx, method, fullUrl, bytes.NewReader(payloadBytes)) req, err := http.NewRequestWithContext(ctx, method, fullUrl, bytes.NewReader(payloadBytes))
if err != nil { if err != nil {
@@ -244,7 +243,7 @@ func parsePfsenseHttpResponse[T any](resp *http.Response, err error) PfsenseApiR
if err != nil { if err != nil {
return errToPfsenseApiResponse[T](PfsenseApiErrorCodeResponseDecodeFailed, "failed to read response body", err) return errToPfsenseApiResponse[T](PfsenseApiErrorCodeResponseDecodeFailed, "failed to read response body", err)
} }
// Close the body after reading // Close the body after reading
defer func() { defer func() {
if err := resp.Body.Close(); err != nil { if err := resp.Body.Close(); err != nil {
@@ -274,7 +273,7 @@ func parsePfsenseHttpResponse[T any](resp *http.Response, err error) PfsenseApiR
"method", resp.Request.Method, "method", resp.Request.Method,
"body_preview", bodyPreview, "body_preview", bodyPreview,
"error", err) "error", err)
return errToPfsenseApiResponse[T](PfsenseApiErrorCodeResponseDecodeFailed, return errToPfsenseApiResponse[T](PfsenseApiErrorCodeResponseDecodeFailed,
fmt.Sprintf("failed to decode response (status %d, content-type: %s): %v", resp.StatusCode, contentType, err), err) fmt.Sprintf("failed to decode response (status %d, content-type: %s): %v", resp.StatusCode, contentType, err), err)
} }
@@ -406,12 +405,11 @@ func (p *PfsenseApiClient) Update(
func (p *PfsenseApiClient) Delete( func (p *PfsenseApiClient) Delete(
ctx context.Context, ctx context.Context,
command string, command string,
opts *PfsenseRequestOptions,
) PfsenseApiResponse[EmptyResponse] { ) PfsenseApiResponse[EmptyResponse] {
apiCtx, cancel := context.WithTimeout(ctx, p.cfg.GetApiTimeout()) apiCtx, cancel := context.WithTimeout(ctx, p.cfg.GetApiTimeout())
defer cancel() defer cancel()
fullUrl := opts.GetPath(p.getFullPath(command)) fullUrl := p.getFullPath(command)
req, err := p.prepareDeleteRequest(apiCtx, fullUrl) req, err := p.prepareDeleteRequest(apiCtx, fullUrl)
if err != nil { if err != nil {
@@ -427,3 +425,4 @@ func (p *PfsenseApiClient) Delete(
} }
// endregion API-client // endregion API-client