diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1aba90f..007f958 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -28,3 +28,8 @@ updates: patch: update-types: - patch + + - package-ecosystem: "docker" + directory: / + schedule: + interval: weekly diff --git a/Dockerfile b/Dockerfile index e904d89..f850ece 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,7 @@ COPY --from=builder /build/dist/wg-portal / ###### # Final image ###### -FROM alpine:3.19 +FROM alpine:3.22 # Install OS-level dependencies RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools # Setup timezone diff --git a/cmd/wg-portal/main.go b/cmd/wg-portal/main.go index 753cb81..97f0b67 100644 --- a/cmd/wg-portal/main.go +++ b/cmd/wg-portal/main.go @@ -88,6 +88,7 @@ func main() { authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager) internal.AssertNoError(err) + authenticator.StartBackgroundJobs(ctx) webAuthn, err := auth.NewWebAuthnAuthenticator(cfg, eventBus, userManager) internal.AssertNoError(err) diff --git a/docs/documentation/configuration/overview.md b/docs/documentation/configuration/overview.md index 70496aa..dd20d79 100644 --- a/docs/documentation/configuration/overview.md +++ b/docs/documentation/configuration/overview.md @@ -38,6 +38,7 @@ advanced: rule_prio_offset: 20000 route_table_offset: 20000 api_admin_only: true + limit_additional_user_peers: 0 database: debug: false @@ -75,6 +76,7 @@ auth: webauthn: enabled: true min_password_length: 16 + hide_login_form: false web: listening_address: :8888 @@ -215,6 +217,10 @@ Additional or more specialized configuration options for logging and interface c - **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). +### `limit_additional_user_peers` +- **Default:** `0` +- **Description:** Limit additional peers a normal user can create. `0` means unlimited. + --- ## Database @@ -349,6 +355,12 @@ Some core authentication options are shared across all providers, while others a 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 @@ -661,18 +673,7 @@ Without a valid `external_url`, the login process may fail due to CSRF protectio ## Webhook The webhook section allows you to configure a webhook that is called on certain events in WireGuard Portal. -A JSON object is sent in a POST request to the webhook URL with the following structure: -```json -{ - "event": "peer_created", - "entity": "peer", - "identifier": "the-peer-identifier", - "payload": { - // The payload of the event, e.g. peer data. - // Check the API documentation for the exact structure. - } -} -``` +Further details can be found in the [usage documentation](../usage/webhooks.md). ### `url` - **Default:** *(empty)* diff --git a/docs/documentation/usage/webhooks.md b/docs/documentation/usage/webhooks.md new file mode 100644 index 0000000..1d0c692 --- /dev/null +++ b/docs/documentation/usage/webhooks.md @@ -0,0 +1,285 @@ + +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 models are supported for webhook events: + +- `user`: WireGuard Portal users support creation, update, or deletion events. +- `peer`: Peers support creation, update, or deletion events. Via the `peer_metric` entity, you can also receive connection status updates. +- `peer_metric`: Peer metrics support connection status updates, such as when a peer connects or disconnects. +- `interface`: WireGuard interfaces support creation, update, or deletion events. + +## 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", // The event type, e.g. "create", "update", "delete", "connect", "disconnect" + "entity": "user", // The entity type, e.g. "user", "peer", "peer_metric", "interface" + "identifier": "the-user-identifier", // Unique identifier of the entity, e.g. user ID or peer ID + "payload": { + // The payload of the event, e.g. a Peer model. + // Detailed model descriptions are provided below. + } +} +``` + +### Payload Models + +All payload models are encoded as JSON objects. Fields with empty values might be omitted in the payload. + +#### User Payload (entity: `user`) + +| JSON Field | Type | Description | +|----------------|-------------|-----------------------------------| +| CreatedBy | string | Creator identifier | +| UpdatedBy | string | Last updater identifier | +| CreatedAt | time.Time | Time of creation | +| UpdatedAt | time.Time | Time of last update | +| Identifier | string | Unique user identifier | +| Email | string | User email | +| Source | string | Authentication source | +| ProviderName | string | Name of auth provider | +| IsAdmin | bool | Whether user has admin privileges | +| Firstname | string | User's first name (optional) | +| Lastname | string | User's last name (optional) | +| Phone | string | Contact phone number (optional) | +| Department | string | User's department (optional) | +| Notes | string | Additional notes (optional) | +| Disabled | *time.Time | When user was disabled | +| DisabledReason | string | Reason for deactivation | +| Locked | *time.Time | When user account was locked | +| LockedReason | string | Reason for being locked | + + +#### Peer Payload (entity: `peer`) + +| JSON Field | Type | Description | +|----------------------|------------|----------------------------------------| +| CreatedBy | string | Creator identifier | +| UpdatedBy | string | Last updater identifier | +| CreatedAt | time.Time | Creation timestamp | +| UpdatedAt | time.Time | Last update timestamp | +| Endpoint | string | Peer endpoint address | +| EndpointPublicKey | string | Public key of peer endpoint | +| AllowedIPsStr | string | Allowed IPs | +| ExtraAllowedIPsStr | string | Extra allowed IPs | +| PresharedKey | string | Pre-shared key for encryption | +| PersistentKeepalive | int | Keepalive interval in seconds | +| DisplayName | string | Display name of the peer | +| Identifier | string | Unique identifier | +| UserIdentifier | string | Associated user ID (optional) | +| InterfaceIdentifier | string | Interface this peer is attached to | +| Disabled | *time.Time | When the peer was disabled | +| DisabledReason | string | Reason for being disabled | +| ExpiresAt | *time.Time | Expiration date | +| Notes | string | Notes for this peer | +| AutomaticallyCreated | bool | Whether peer was auto-generated | +| PrivateKey | string | Peer private key | +| PublicKey | string | Peer public key | +| InterfaceType | string | Type of the peer interface | +| Addresses | []string | IP addresses | +| CheckAliveAddress | string | Address used for alive checks | +| DnsStr | string | DNS servers | +| DnsSearchStr | string | DNS search domains | +| Mtu | int | MTU (Maximum Transmission Unit) | +| FirewallMark | uint32 | Firewall mark (optional) | +| RoutingTable | string | Custom routing table (optional) | +| PreUp | string | Command before bringing up interface | +| PostUp | string | Command after bringing up interface | +| PreDown | string | Command before bringing down interface | +| PostDown | string | Command after bringing down interface | + + +#### Interface Payload (entity: `interface`) + +| JSON Field | Type | Description | +|----------------------------|------------|----------------------------------------| +| CreatedBy | string | Creator identifier | +| UpdatedBy | string | Last updater identifier | +| CreatedAt | time.Time | Creation timestamp | +| UpdatedAt | time.Time | Last update timestamp | +| Identifier | string | Unique identifier | +| PrivateKey | string | Private key for the interface | +| PublicKey | string | Public key for the interface | +| ListenPort | int | Listening port | +| Addresses | []string | IP addresses | +| DnsStr | string | DNS servers | +| DnsSearchStr | string | DNS search domains | +| Mtu | int | MTU (Maximum Transmission Unit) | +| FirewallMark | uint32 | Firewall mark | +| RoutingTable | string | Custom routing table | +| PreUp | string | Command before bringing up interface | +| PostUp | string | Command after bringing up interface | +| PreDown | string | Command before bringing down interface | +| PostDown | string | Command after bringing down interface | +| SaveConfig | bool | Whether to save config to file | +| DisplayName | string | Human-readable name | +| Type | string | Type of interface | +| DriverType | string | Driver used | +| Disabled | *time.Time | When the interface was disabled | +| DisabledReason | string | Reason for being disabled | +| PeerDefNetworkStr | string | Default peer network configuration | +| PeerDefDnsStr | string | Default peer DNS servers | +| PeerDefDnsSearchStr | string | Default peer DNS search domains | +| PeerDefEndpoint | string | Default peer endpoint | +| PeerDefAllowedIPsStr | string | Default peer allowed IPs | +| PeerDefMtu | int | Default peer MTU | +| PeerDefPersistentKeepalive | int | Default keepalive value | +| PeerDefFirewallMark | uint32 | Default firewall mark for peers | +| PeerDefRoutingTable | string | Default routing table for peers | +| PeerDefPreUp | string | Default peer pre-up command | +| PeerDefPostUp | string | Default peer post-up command | +| PeerDefPreDown | string | Default peer pre-down command | +| PeerDefPostDown | string | Default peer post-down command | + + +#### Peer Metrics Payload (entity: `peer_metric`) + +| JSON Field | Type | Description | +|------------|------------|----------------------------| +| Status | PeerStatus | Current status of the peer | +| Peer | Peer | Peer data | + +`PeerStatus` sub-structure: + +| JSON Field | Type | Description | +|------------------|------------|------------------------------| +| UpdatedAt | time.Time | Time of last status update | +| IsConnected | bool | Is peer currently connected | +| IsPingable | bool | Can peer be pinged | +| LastPing | *time.Time | Time of last successful ping | +| BytesReceived | uint64 | Bytes received from peer | +| BytesTransmitted | uint64 | Bytes sent to peer | +| Endpoint | string | Last known endpoint | +| LastHandshake | *time.Time | Last successful handshake | +| LastSessionStart | *time.Time | Time the last session began | + + +### Example Payloads + +The following payload is an example of a webhook event when a peer connects to the VPN: + +```json +{ + "event": "connect", + "entity": "peer_metric", + "identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", + "payload": { + "Status": { + "UpdatedAt": "2025-06-27T22:20:08.734900034+02:00", + "IsConnected": true, + "IsPingable": false, + "BytesReceived": 212, + "BytesTransmitted": 2884, + "Endpoint": "10.55.66.77:58756", + "LastHandshake": "2025-06-27T22:19:46.580842776+02:00", + "LastSessionStart": "2025-06-27T22:19:46.580842776+02:00" + }, + "Peer": { + "CreatedBy": "admin@wgportal.local", + "UpdatedBy": "admin@wgportal.local", + "CreatedAt": "2025-06-26T21:43:49.251839574+02:00", + "UpdatedAt": "2025-06-27T22:18:39.67763985+02:00", + "Endpoint": "10.55.66.1:51820", + "EndpointPublicKey": "eiVibpi3C2PUPcx2kwA5s09OgHx7AEaKMd33k0LQ5mM=", + "AllowedIPsStr": "10.11.12.0/24,fdfd:d3ad:c0de:1234::/64", + "ExtraAllowedIPsStr": "", + "PresharedKey": "p9DDeLUSLOdQcjS8ZsBAiqUzwDIUvTyzavRZFuzhvyE=", + "PersistentKeepalive": 16, + "DisplayName": "Peer Fb5TaziA", + "Identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", + "UserIdentifier": "admin@wgportal.local", + "InterfaceIdentifier": "wgTesting", + "AutomaticallyCreated": false, + "PrivateKey": "QBFNBe+7J49ergH0ze2TGUJMFrL/2bOL50Z2cgluYW8=", + "PublicKey": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", + "InterfaceType": "client", + "Addresses": [ + "10.11.12.10/32", + "fdfd:d3ad:c0de:1234::a/128" + ], + "CheckAliveAddress": "", + "DnsStr": "", + "DnsSearchStr": "", + "Mtu": 1420 + } + } +} +``` + +Here is another example of a webhook event when a peer is updated: + +```json +{ + "event": "update", + "entity": "peer", + "identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", + "payload": { + "CreatedBy": "admin@wgportal.local", + "UpdatedBy": "admin@wgportal.local", + "CreatedAt": "2025-06-26T21:43:49.251839574+02:00", + "UpdatedAt": "2025-06-27T22:18:39.67763985+02:00", + "Endpoint": "10.55.66.1:51820", + "EndpointPublicKey": "eiVibpi3C2PUPcx2kwA5s09OgHx7AEaKMd33k0LQ5mM=", + "AllowedIPsStr": "10.11.12.0/24,fdfd:d3ad:c0de:1234::/64", + "ExtraAllowedIPsStr": "", + "PresharedKey": "p9DDeLUSLOdQcjS8ZsBAiqUzwDIUvTyzavRZFuzhvyE=", + "PersistentKeepalive": 16, + "DisplayName": "Peer Fb5TaziA", + "Identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", + "UserIdentifier": "admin@wgportal.local", + "InterfaceIdentifier": "wgTesting", + "AutomaticallyCreated": false, + "PrivateKey": "QBFNBe+7J49ergH0ze2TGUJMFrL/2bOL50Z2cgluYW8=", + "PublicKey": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", + "InterfaceType": "client", + "Addresses": [ + "10.11.12.10/32", + "fdfd:d3ad:c0de:1234::a/128" + ], + "CheckAliveAddress": "", + "DnsStr": "", + "DnsSearchStr": "", + "Mtu": 1420 + } +} +``` \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index fd8033f..287ef99 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,5 +1,5 @@ - + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index afbb37b..db118ca 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,8 +14,8 @@ "@popperjs/core": "^2.11.8", "@simplewebauthn/browser": "^13.1.0", "@vojtechlanka/vue-tags-input": "^3.1.1", - "bootstrap": "^5.3.5", - "bootswatch": "^5.3.5", + "bootstrap": "^5.3.7", + "bootswatch": "^5.3.7", "flag-icons": "^7.3.2", "ip-address": "^10.0.1", "is-cidr": "^5.1.1", @@ -1048,9 +1048,9 @@ } }, "node_modules/bootstrap": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz", - "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz", + "integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==", "funding": [ { "type": "github", @@ -1067,9 +1067,9 @@ } }, "node_modules/bootswatch": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.3.5.tgz", - "integrity": "sha512-1z8LNoUL5NHmv/hNROALQ6qtjw9OJIjMgP8ovBlIft+oI15b/mvnzxGL896iO9LtoDZH0Vdm+D2YW+j03GduSg==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.3.7.tgz", + "integrity": "sha512-n0X99+Jmpmd4vgkli5KwMOuAkgdyUPhq7cIAwoGXbM6WhE/mmkWACfxpr7WZeG9Pdx509Ndi+2K1HlzXXOr8/Q==", "license": "MIT" }, "node_modules/buffer-builder": { diff --git a/frontend/package.json b/frontend/package.json index 0be893a..e226a95 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,8 +14,8 @@ "@popperjs/core": "^2.11.8", "@simplewebauthn/browser": "^13.1.0", "@vojtechlanka/vue-tags-input": "^3.1.1", - "bootstrap": "^5.3.5", - "bootswatch": "^5.3.5", + "bootstrap": "^5.3.7", + "bootswatch": "^5.3.7", "flag-icons": "^7.3.2", "ip-address": "^10.0.1", "is-cidr": "^5.1.1", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index e6e20d2..cddea54 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -14,6 +14,10 @@ const settings = settingsStore() onMounted(async () => { console.log("Starting WireGuard Portal frontend..."); + // restore theme from localStorage + const theme = localStorage.getItem('wgTheme') || 'light'; + document.documentElement.setAttribute('data-bs-theme', theme); + await sec.LoadSecurityProperties(); await auth.LoadProviders(); @@ -40,6 +44,13 @@ const switchLanguage = function (lang) { } } +const switchTheme = function (theme) { + if (document.documentElement.getAttribute('data-bs-theme') !== theme) { + localStorage.setItem('wgTheme', theme); + document.documentElement.setAttribute('data-bs-theme', theme); + } +} + const languageFlag = computed(() => { // `this` points to the component instance let lang = appGlobal.$i18n.locale.toLowerCase(); @@ -125,6 +136,24 @@ const userDisplayName = computed(() => { + @@ -141,7 +170,7 @@ const userDisplayName = computed(() => {
- diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 0291ca7..be87e56 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -13,21 +13,21 @@ const auth = authStore()

{{ $t('home.abstract') }}

-
+

{{ $t('home.profiles.headline') }}

{{ $t('home.profiles.abstract') }}


-

{{ $t('home.profiles.content') }}

+

{{ $t('home.profiles.content') }}

{{ $t('home.profiles.button') }}

-
+

{{ $t('home.admin.headline') }}

{{ $t('home.admin.abstract') }}


-

{{ $t('home.admin.content') }}

+

{{ $t('home.admin.content') }}

{{ $t('home.admin.button-admin') }} diff --git a/frontend/src/views/InterfaceView.vue b/frontend/src/views/InterfaceView.vue index 56ef34d..b28cf5f 100644 --- a/frontend/src/views/InterfaceView.vue +++ b/frontend/src/views/InterfaceView.vue @@ -142,7 +142,7 @@ onMounted(async () => {

- - +
@@ -459,3 +459,5 @@ onMounted(async () => {
+ diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index d5037e2..9003b83 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -16,7 +16,10 @@ const password = ref("") const usernameInvalid = computed(() => username.value === "") const passwordInvalid = computed(() => password.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() @@ -98,7 +101,7 @@ const externalLogin = function (provider) {
-
+
@@ -118,19 +121,40 @@ const externalLogin = function (provider) {
-
-
-
+
-
+
+
+ + +
+
+ +
+
+
+
+
+
+ +
+
+ +
diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue index a5daec2..ea73fab 100644 --- a/frontend/src/views/ProfileView.vue +++ b/frontend/src/views/ProfileView.vue @@ -65,7 +65,7 @@ onMounted(async () => {
-
@@ -73,7 +73,7 @@ onMounted(async () => {
- - +
diff --git a/go.mod b/go.mod index b83e54c..dfa6bbf 100644 --- a/go.mod +++ b/go.mod @@ -4,32 +4,32 @@ go 1.24.0 require ( github.com/a8m/envsubst v1.4.3 - github.com/alexedwards/scs/v2 v2.8.0 + github.com/alexedwards/scs/v2 v2.9.0 github.com/coreos/go-oidc/v3 v3.14.1 github.com/glebarez/sqlite v1.11.0 github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-pkgz/routegroup v1.4.1 - github.com/go-playground/validator/v10 v10.26.0 - github.com/go-webauthn/webauthn v0.12.3 + github.com/go-playground/validator/v10 v10.27.0 + github.com/go-webauthn/webauthn v0.13.4 github.com/google/uuid v1.6.0 github.com/prometheus-community/pro-bing v0.7.0 github.com/prometheus/client_golang v1.22.0 github.com/stretchr/testify v1.10.0 - github.com/swaggo/swag v1.16.4 + github.com/swaggo/swag v1.16.5 github.com/vardius/message-bus v1.1.5 github.com/vishvananda/netlink v1.3.1 github.com/xhit/go-simple-mail/v2 v2.16.0 github.com/yeqown/go-qrcode/v2 v2.2.5 github.com/yeqown/go-qrcode/writer/compressed v1.0.1 - golang.org/x/crypto v0.38.0 + golang.org/x/crypto v0.40.0 golang.org/x/oauth2 v0.30.0 - golang.org/x/sys v0.33.0 + golang.org/x/sys v0.34.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 gopkg.in/yaml.v3 v3.0.1 - gorm.io/driver/mysql v1.5.7 - gorm.io/driver/postgres v1.5.11 - gorm.io/driver/sqlserver v1.5.4 - gorm.io/gorm v1.26.1 + gorm.io/driver/mysql v1.6.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/driver/sqlserver v1.6.1 + gorm.io/gorm v1.30.0 ) require ( @@ -40,7 +40,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect @@ -53,12 +53,12 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.9.2 // indirect github.com/go-test/deep v1.1.1 // indirect - github.com/go-webauthn/x v0.1.20 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/go-webauthn/x v0.1.23 // indirect + github.com/golang-jwt/jwt/v5 v5.2.3 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-tpm v0.9.3 // indirect + github.com/google/go-tpm v0.9.5 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.4 // indirect @@ -73,7 +73,7 @@ require ( github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // 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.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect @@ -87,10 +87,11 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/tools v0.32.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.34.0 // indirect golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect google.golang.org/protobuf v1.36.6 // indirect modernc.org/libc v1.63.0 // indirect diff --git a/go.sum b/go.sum index c427c3f..d905e33 100644 --- a/go.sum +++ b/go.sum @@ -2,15 +2,13 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 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.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -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/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.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/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.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +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 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/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= @@ -20,7 +18,6 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h 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/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -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/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -29,14 +26,15 @@ github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= -github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= +github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90= +github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -44,8 +42,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/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/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= -github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.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/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= @@ -74,34 +72,34 @@ 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/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/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -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-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 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-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-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE= -github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY= -github.com/go-webauthn/x v0.1.20 h1:brEBDqfiPtNNCdS/peu8gARtq8fIPsHz0VzpPjGvgiw= -github.com/go-webauthn/x v0.1.20/go.mod h1:n/gAc8ssZJGATM0qThE+W+vfgXiMedsWi3wf/C4lld0= +github.com/go-webauthn/webauthn v0.13.4 h1:q68qusWPcqHbg9STSxBLBHnsKaLxNO0RnVKaAqMuAuQ= +github.com/go-webauthn/webauthn v0.13.4/go.mod h1:MglN6OH9ECxvhDqoq1wMoF6P6JRYDiQpC9nc5OomQmI= +github.com/go-webauthn/x v0.1.23 h1:9lEO0s+g8iTyz5Vszlg/rXTGrx3CjcD0RZQ1GPZCaxI= +github.com/go-webauthn/x v0.1.23/go.mod h1:AJd3hI7NfEp/4fI6T4CHD753u91l510lglU7/NMN6+E= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +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.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/google/go-cmp v0.5.9/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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc= -github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= +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/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= @@ -139,8 +137,11 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -157,9 +158,8 @@ 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/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= 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 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.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I= +github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo= 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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -173,6 +173,7 @@ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm 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/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA= @@ -187,11 +188,13 @@ github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2b github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -199,10 +202,11 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ 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.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= -github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/swaggo/swag v1.16.5 h1:nMf2fEV1TetMTJb4XzD0Lz7jFfKJmJKGTygEey8NSxM= +github.com/swaggo/swag v1.16.5/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5JNfgjzpzvYLHjH0QOgPZPYnRWGA= github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= @@ -228,18 +232,25 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +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.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= 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/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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +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.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.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -250,18 +261,26 @@ 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.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -275,11 +294,16 @@ 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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -287,9 +311,13 @@ 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.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.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 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.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= 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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -300,14 +328,19 @@ 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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +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.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= 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.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.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +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.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= 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/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= @@ -325,16 +358,14 @@ 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= -gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= -gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= -gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= -gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw= -gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY= +gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA= modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc= diff --git a/internal/adapters/metrics.go b/internal/adapters/metrics.go index d9bea6e..6b9a4b7 100644 --- a/internal/adapters/metrics.go +++ b/internal/adapters/metrics.go @@ -133,5 +133,5 @@ func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerS } m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived)) 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)) } diff --git a/internal/adapters/wireguard.go b/internal/adapters/wireguard.go index 6c15771..327d54a 100644 --- a/internal/adapters/wireguard.go +++ b/internal/adapters/wireguard.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log/slog" "os" "github.com/vishvananda/netlink" @@ -16,8 +17,9 @@ import ( // WgRepo implements all low-level WireGuard interactions. type WgRepo struct { - wg lowlevel.WireGuardClient - nl lowlevel.NetlinkClient + wg lowlevel.WireGuardClient + nl lowlevel.NetlinkClient + log *slog.Logger } // NewWireGuardRepository creates a new WgRepo instance. @@ -31,8 +33,9 @@ func NewWireGuardRepository() *WgRepo { nl := &lowlevel.NetlinkManager{} repo := &WgRepo{ - wg: wg, - nl: nl, + wg: wg, + nl: nl, + log: slog.Default().With(slog.String("adapter", "wireguard")), } return repo @@ -40,8 +43,10 @@ func NewWireGuardRepository() *WgRepo { // GetInterfaces returns all existing WireGuard interfaces. func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) { + r.log.Debug("getting all interfaces") devices, err := r.wg.Devices() if err != nil { + r.log.Error("failed to get devices", "error", err) return nil, fmt.Errorf("device list error: %w", err) } @@ -60,14 +65,17 @@ func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, e // GetInterface returns the interface with the given id. // If no interface is found, an error os.ErrNotExist is returned. func (r *WgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) { + r.log.Debug("getting interface", "id", id) return r.getInterface(id) } // GetPeers returns all peers associated with the given interface id. // If the requested interface is found, an error os.ErrNotExist is returned. func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) { + r.log.Debug("getting peers for interface", "deviceId", deviceId) device, err := r.wg.Device(string(deviceId)) if err != nil { + r.log.Error("failed to get device", "deviceId", deviceId, "error", err) return nil, fmt.Errorf("device error: %w", err) } @@ -90,6 +98,7 @@ func (r *WgRepo) GetPeer( deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier, ) (*domain.PhysicalPeer, error) { + r.log.Debug("getting peer", "deviceId", deviceId, "peerId", id) return r.getPeer(deviceId, id) } @@ -174,25 +183,31 @@ func (r *WgRepo) SaveInterface( id domain.InterfaceIdentifier, updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error), ) error { + r.log.Debug("saving interface", "id", id) physicalInterface, err := r.getOrCreateInterface(id) if err != nil { + r.log.Error("failed to get or create interface", "id", id, "error", err) return err } if updateFunc != nil { physicalInterface, err = updateFunc(physicalInterface) if err != nil { + r.log.Error("interface update function failed", "id", id, "error", err) return err } } if err := r.updateLowLevelInterface(physicalInterface); err != nil { + r.log.Error("failed to update low level interface", "id", id, "error", err) return err } if err := r.updateWireGuardInterface(physicalInterface); err != nil { + r.log.Error("failed to update wireguard interface", "id", id, "error", err) return err } + r.log.Debug("successfully saved interface", "id", id) return nil } @@ -323,10 +338,13 @@ func (r *WgRepo) updateWireGuardInterface(pi *domain.PhysicalInterface) error { // DeleteInterface deletes the interface with the given id. // If the requested interface is found, no error is returned. func (r *WgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error { + r.log.Debug("deleting interface", "id", id) if err := r.deleteLowLevelInterface(id); err != nil { + r.log.Error("failed to delete low level interface", "id", id, "error", err) return err } + r.log.Debug("successfully deleted interface", "id", id) return nil } @@ -356,20 +374,25 @@ func (r *WgRepo) SavePeer( id domain.PeerIdentifier, updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error), ) error { + r.log.Debug("saving peer", "deviceId", deviceId, "peerId", id) physicalPeer, err := r.getOrCreatePeer(deviceId, id) if err != nil { + r.log.Error("failed to get or create peer", "deviceId", deviceId, "peerId", id, "error", err) return err } physicalPeer, err = updateFunc(physicalPeer) if err != nil { + r.log.Error("peer update function failed", "deviceId", deviceId, "peerId", id, "error", err) return err } if err := r.updatePeer(deviceId, physicalPeer); err != nil { + r.log.Error("failed to update peer", "deviceId", deviceId, "peerId", id, "error", err) return err } + r.log.Debug("successfully saved peer", "deviceId", deviceId, "peerId", id) return nil } @@ -441,6 +464,7 @@ func (r *WgRepo) updatePeer(deviceId domain.InterfaceIdentifier, pp *domain.Phys err := r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}}) if err != nil { + r.log.Error("failed to configure device for peer update", "deviceId", deviceId, "peerId", pp.Identifier, "error", err) return err } @@ -450,15 +474,20 @@ func (r *WgRepo) updatePeer(deviceId domain.InterfaceIdentifier, pp *domain.Phys // DeletePeer deletes the peer with the given id. // If the requested interface or peer is found, no error is returned. func (r *WgRepo) DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error { + r.log.Debug("deleting peer", "deviceId", deviceId, "peerId", id) if !id.IsPublicKey() { - return errors.New("invalid public key") + err := errors.New("invalid public key") + r.log.Error("invalid peer id", "peerId", id, "error", err) + return err } err := r.deletePeer(deviceId, id) if err != nil { + r.log.Error("failed to delete peer", "deviceId", deviceId, "peerId", id, "error", err) return err } + r.log.Debug("successfully deleted peer", "deviceId", deviceId, "peerId", id) return nil } @@ -470,6 +499,7 @@ func (r *WgRepo) deletePeer(deviceId domain.InterfaceIdentifier, id domain.PeerI err := r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}}) if err != nil { + r.log.Error("failed to configure device for peer deletion", "deviceId", deviceId, "peerId", id, "error", err) return err } diff --git a/internal/app/api/core/assets/doc/v0_swagger.json b/internal/app/api/core/assets/doc/v0_swagger.json index 0658046..fc2ebc8 100644 --- a/internal/app/api/core/assets/doc/v0_swagger.json +++ b/internal/app/api/core/assets/doc/v0_swagger.json @@ -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": { "post": { "produces": [ @@ -275,52 +321,6 @@ } } }, - "/auth/{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/{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" - } - } - } - } - } - }, "/config/frontend.js": { "get": { "produces": [ @@ -819,6 +819,12 @@ "schema": { "$ref": "#/definitions/model.PeerMailRequest" } + }, + { + "type": "string", + "description": "The configuration style", + "name": "style", + "in": "query" } ], "responses": { @@ -858,6 +864,12 @@ "name": "id", "in": "path", "required": true + }, + { + "type": "string", + "description": "The configuration style", + "name": "style", + "in": "query" } ], "responses": { @@ -899,6 +911,12 @@ "name": "id", "in": "path", "required": true + }, + { + "type": "string", + "description": "The configuration style", + "name": "style", + "in": "query" } ], "responses": { @@ -2231,9 +2249,15 @@ "ApiAdminOnly": { "type": "boolean" }, + "LoginFormVisible": { + "type": "boolean" + }, "MailLinkOnly": { "type": "boolean" }, + "MinPasswordLength": { + "type": "integer" + }, "PersistentConfigSupported": { "type": "boolean" }, diff --git a/internal/app/api/core/assets/doc/v0_swagger.yaml b/internal/app/api/core/assets/doc/v0_swagger.yaml index e8778b2..479cb8d 100644 --- a/internal/app/api/core/assets/doc/v0_swagger.yaml +++ b/internal/app/api/core/assets/doc/v0_swagger.yaml @@ -381,8 +381,12 @@ definitions: properties: ApiAdminOnly: type: boolean + LoginFormVisible: + type: boolean MailLinkOnly: type: boolean + MinPasswordLength: + type: integer PersistentConfigSupported: type: boolean SelfProvisioning: @@ -472,7 +476,22 @@ paths: summary: Get all available audit entries. Ordered by timestamp. tags: - 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: operationId: auth_handleOauthCallbackGet produces: @@ -487,7 +506,7 @@ paths: summary: Handle the OAuth callback. tags: - Authentication - /auth/{provider}/init: + /auth/login/{provider}/init: get: operationId: auth_handleOauthInitiateGet produces: @@ -502,21 +521,6 @@ paths: summary: Initiate the OAuth login flow. tags: - 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: post: operationId: auth_handleLogoutPost @@ -1068,6 +1072,10 @@ paths: required: true schema: $ref: '#/definitions/model.PeerMailRequest' + - description: The configuration style + in: query + name: style + type: string produces: - application/json responses: @@ -1093,6 +1101,10 @@ paths: name: id required: true type: string + - description: The configuration style + in: query + name: style + type: string produces: - image/png - application/json @@ -1121,6 +1133,10 @@ paths: name: id required: true type: string + - description: The configuration style + in: query + name: style + type: string produces: - application/json responses: diff --git a/internal/app/api/core/server.go b/internal/app/api/core/server.go index fe0d1e6..87b5519 100644 --- a/internal/app/api/core/server.go +++ b/internal/app/api/core/server.go @@ -100,6 +100,7 @@ func (s *Server) Run(ctx context.Context, listenAddress string) { srvContext, cancelFn := context.WithCancel(ctx) go func() { var err error + slog.Debug("starting server", "certFile", s.cfg.Web.CertFile, "keyFile", s.cfg.Web.KeyFile) if s.cfg.Web.CertFile != "" && s.cfg.Web.KeyFile != "" { err = srv.ListenAndServeTLS(s.cfg.Web.CertFile, s.cfg.Web.KeyFile) } else { diff --git a/internal/app/api/v0/backend/peer_service.go b/internal/app/api/v0/backend/peer_service.go index f590693..ab4a7a8 100644 --- a/internal/app/api/v0/backend/peer_service.go +++ b/internal/app/api/v0/backend/peer_service.go @@ -27,12 +27,12 @@ type PeerServicePeerManager interface { } type PeerServiceConfigFileManager interface { - GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) - GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) + GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) + GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) } type PeerServiceMailManager interface { - SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error + SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error } // endregion dependencies @@ -95,16 +95,24 @@ func (p PeerService) DeletePeer(ctx context.Context, id domain.PeerIdentifier) e return p.peers.DeletePeer(ctx, id) } -func (p PeerService) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) { - return p.configFile.GetPeerConfig(ctx, id) +func (p PeerService) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) { + return p.configFile.GetPeerConfig(ctx, id, style) } -func (p PeerService) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) { - return p.configFile.GetPeerConfigQrCode(ctx, id) +func (p PeerService) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) ( + io.Reader, + error, +) { + return p.configFile.GetPeerConfigQrCode(ctx, id, style) } -func (p PeerService) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error { - return p.mailer.SendPeerEmail(ctx, linkOnly, peers...) +func (p PeerService) SendPeerEmail( + ctx context.Context, + linkOnly bool, + style string, + peers ...domain.PeerIdentifier, +) error { + return p.mailer.SendPeerEmail(ctx, linkOnly, style, peers...) } func (p PeerService) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error) { diff --git a/internal/app/api/v0/handlers/endpoint_authentication.go b/internal/app/api/v0/handlers/endpoint_authentication.go index de412e3..26532b0 100644 --- a/internal/app/api/v0/handlers/endpoint_authentication.go +++ b/internal/app/api/v0/handlers/endpoint_authentication.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "log/slog" "net/http" "net/url" "strconv" @@ -189,7 +190,7 @@ func (e AuthEndpoint) handleSessionInfoGet() http.HandlerFunc { // @Summary Initiate the OAuth login flow. // @Produce json // @Success 200 {object} []model.LoginProviderInfo -// @Router /auth/{provider}/init [get] +// @Router /auth/login/{provider}/init [get] func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { currentSession := e.session.GetData(r.Context()) @@ -234,6 +235,8 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc { authCodeUrl, state, nonce, err := e.authService.OauthLoginStep1(context.Background(), provider) if err != nil { + slog.Debug("failed to create oauth auth code URL", + "provider", provider, "error", err) if autoRedirect && e.isValidReturnUrl(returnTo) { redirectToReturn() } else { @@ -268,7 +271,7 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc { // @Summary Handle the OAuth callback. // @Produce json // @Success 200 {object} []model.LoginProviderInfo -// @Router /auth/{provider}/callback [get] +// @Router /auth/login/{provider}/callback [get] func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { currentSession := e.session.GetData(r.Context()) @@ -306,6 +309,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc { oauthState := request.Query(r, "state") 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()) { redirectToReturn() } else { @@ -315,6 +320,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc { return } 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()) { redirectToReturn() } else { @@ -324,11 +331,13 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc { 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, oauthCode) cancel() if err != nil { + slog.Debug("failed to process oauth code", + "provider", provider, "state", oauthState, "error", err) if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) { redirectToReturn() } else { diff --git a/internal/app/api/v0/handlers/endpoint_config.go b/internal/app/api/v0/handlers/endpoint_config.go index 1bc8a25..1fe3afb 100644 --- a/internal/app/api/v0/handlers/endpoint_config.go +++ b/internal/app/api/v0/handlers/endpoint_config.go @@ -124,11 +124,14 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc { } + hasSocialLogin := len(e.cfg.Auth.OAuth) > 0 || len(e.cfg.Auth.OpenIDConnect) > 0 || e.cfg.Auth.WebAuthn.Enabled + // 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, AvailableBackends: []model.SettingsBackendNames{}, // return an empty list instead of null + LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin, }) } else { respond.JSON(w, http.StatusOK, model.Settings{ @@ -139,6 +142,7 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc { WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled, MinPasswordLength: e.cfg.Auth.MinPasswordLength, AvailableBackends: controllerFn(), + LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin, }) } } diff --git a/internal/app/api/v0/handlers/endpoint_peers.go b/internal/app/api/v0/handlers/endpoint_peers.go index d9a0115..e60a3aa 100644 --- a/internal/app/api/v0/handlers/endpoint_peers.go +++ b/internal/app/api/v0/handlers/endpoint_peers.go @@ -34,11 +34,11 @@ type PeerService interface { // DeletePeer deletes the peer with the given id. DeletePeer(ctx context.Context, id domain.PeerIdentifier) error // GetPeerConfig returns the peer configuration for the given id. - GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) + GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) // GetPeerConfigQrCode returns the peer configuration as qr code for the given id. - GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) + GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) // SendPeerEmail sends the peer configuration via email. - SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error + SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error // GetPeerStats returns the peer stats for the given interface. GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error) } @@ -355,6 +355,7 @@ func (e PeerEndpoint) handleDelete() http.HandlerFunc { // @Summary Get peer configuration as string. // @Produce json // @Param id path string true "The peer identifier" +// @Param style query string false "The configuration style" // @Success 200 {object} string // @Failure 400 {object} model.Error // @Failure 500 {object} model.Error @@ -369,7 +370,9 @@ func (e PeerEndpoint) handleConfigGet() http.HandlerFunc { return } - configTxt, err := e.peerService.GetPeerConfig(r.Context(), domain.PeerIdentifier(id)) + configStyle := e.getConfigStyle(r) + + configTxt, err := e.peerService.GetPeerConfig(r.Context(), domain.PeerIdentifier(id), configStyle) if err != nil { respond.JSON(w, http.StatusInternalServerError, model.Error{ Code: http.StatusInternalServerError, Message: err.Error(), @@ -397,6 +400,7 @@ func (e PeerEndpoint) handleConfigGet() http.HandlerFunc { // @Produce png // @Produce json // @Param id path string true "The peer identifier" +// @Param style query string false "The configuration style" // @Success 200 {file} binary // @Failure 400 {object} model.Error // @Failure 500 {object} model.Error @@ -411,7 +415,9 @@ func (e PeerEndpoint) handleQrCodeGet() http.HandlerFunc { return } - configQr, err := e.peerService.GetPeerConfigQrCode(r.Context(), domain.PeerIdentifier(id)) + configStyle := e.getConfigStyle(r) + + configQr, err := e.peerService.GetPeerConfigQrCode(r.Context(), domain.PeerIdentifier(id), configStyle) if err != nil { respond.JSON(w, http.StatusInternalServerError, model.Error{ Code: http.StatusInternalServerError, Message: err.Error(), @@ -438,6 +444,7 @@ func (e PeerEndpoint) handleQrCodeGet() http.HandlerFunc { // @Summary Send peer configuration via email. // @Produce json // @Param request body model.PeerMailRequest true "The peer mail request data" +// @Param style query string false "The configuration style" // @Success 204 "No content if mail sending was successful" // @Failure 400 {object} model.Error // @Failure 500 {object} model.Error @@ -460,11 +467,13 @@ func (e PeerEndpoint) handleEmailPost() http.HandlerFunc { return } + configStyle := e.getConfigStyle(r) + peerIds := make([]domain.PeerIdentifier, len(req.Identifiers)) for i := range req.Identifiers { peerIds[i] = domain.PeerIdentifier(req.Identifiers[i]) } - if err := e.peerService.SendPeerEmail(r.Context(), req.LinkOnly, peerIds...); err != nil { + if err := e.peerService.SendPeerEmail(r.Context(), req.LinkOnly, configStyle, peerIds...); err != nil { respond.JSON(w, http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()}) return @@ -504,3 +513,11 @@ func (e PeerEndpoint) handleStatsGet() http.HandlerFunc { respond.JSON(w, http.StatusOK, model.NewPeerStats(e.cfg.Statistics.CollectPeerData, stats)) } } + +func (e PeerEndpoint) getConfigStyle(r *http.Request) string { + configStyle := request.QueryDefault(r, "style", domain.ConfigStyleWgQuick) + if configStyle != domain.ConfigStyleWgQuick && configStyle != domain.ConfigStyleRaw { + configStyle = domain.ConfigStyleWgQuick // default to wg-quick style + } + return configStyle +} diff --git a/internal/app/api/v0/model/models.go b/internal/app/api/v0/model/models.go index 9ab0944..07e2eba 100644 --- a/internal/app/api/v0/model/models.go +++ b/internal/app/api/v0/model/models.go @@ -13,6 +13,7 @@ type Settings struct { WebAuthnEnabled bool `json:"WebAuthnEnabled"` MinPasswordLength int `json:"MinPasswordLength"` AvailableBackends []SettingsBackendNames `json:"AvailableBackends"` + LoginFormVisible bool `json:"LoginFormVisible"` } type SettingsBackendNames struct { diff --git a/internal/app/api/v0/model/models_peer.go b/internal/app/api/v0/model/models_peer.go index 9612dc6..4f8f2b3 100644 --- a/internal/app/api/v0/model/models_peer.go +++ b/internal/app/api/v0/model/models_peer.go @@ -198,7 +198,7 @@ func NewPeerStats(enabled bool, src []domain.PeerStatus) *PeerStats { for _, srcStat := range src { stats[string(srcStat.PeerId)] = PeerStatData{ - IsConnected: srcStat.IsConnected(), + IsConnected: srcStat.IsConnected, IsPingable: srcStat.IsPingable, LastPing: srcStat.LastPing, BytesReceived: srcStat.BytesReceived, diff --git a/internal/app/api/v1/backend/provisioning_service.go b/internal/app/api/v1/backend/provisioning_service.go index 4e14ecb..88bbfee 100644 --- a/internal/app/api/v1/backend/provisioning_service.go +++ b/internal/app/api/v1/backend/provisioning_service.go @@ -23,8 +23,8 @@ type ProvisioningServicePeerManagerRepo interface { } type ProvisioningServiceConfigFileManagerRepo interface { - GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) - GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) + GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) + GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) } type ProvisioningService struct { @@ -96,7 +96,7 @@ func (p ProvisioningService) GetPeerConfig(ctx context.Context, peerId domain.Pe return nil, err } - peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier) + peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier, domain.ConfigStyleWgQuick) if err != nil { return nil, err } @@ -119,7 +119,7 @@ func (p ProvisioningService) GetPeerQrPng(ctx context.Context, peerId domain.Pee return nil, err } - peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier) + peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier, domain.ConfigStyleWgQuick) if err != nil { return nil, err } diff --git a/internal/app/auth/auth.go b/internal/app/auth/auth.go index 380fe81..c6e5232 100644 --- a/internal/app/auth/auth.go +++ b/internal/app/auth/auth.go @@ -93,6 +93,8 @@ type Authenticator struct { // URL prefix for the callback endpoints, this is a combination of the external URL and the API prefix callbackUrlPrefix string + callbackUrl *url.URL + users UserManager } @@ -102,82 +104,136 @@ func NewAuthenticator(cfg *config.Auth, extUrl string, bus EventBus, users UserM error, ) { a := &Authenticator{ - cfg: cfg, - bus: bus, - users: users, - callbackUrlPrefix: fmt.Sprintf("%s/api/v0", extUrl), + cfg: cfg, + bus: bus, + users: users, + callbackUrlPrefix: fmt.Sprintf("%s/api/v0", extUrl), + oauthAuthenticators: make(map[string]AuthenticatorOauth, len(cfg.OpenIDConnect)+len(cfg.OAuth)), + ldapAuthenticators: make(map[string]AuthenticatorLdap, len(cfg.Ldap)), } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - err := a.setupExternalAuthProviders(ctx) + parsedExtUrl, err := url.Parse(a.callbackUrlPrefix) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse external URL: %w", err) } + a.callbackUrl = parsedExtUrl return a, nil } -func (a *Authenticator) setupExternalAuthProviders(ctx context.Context) error { - extUrl, err := url.Parse(a.callbackUrlPrefix) - if err != nil { - return fmt.Errorf("failed to parse external url: %w", err) - } +// StartBackgroundJobs starts the background jobs for the authenticator. +// It sets up the external authentication providers (OIDC, OAuth, LDAP) and retries in case of errors. +func (a *Authenticator) StartBackgroundJobs(ctx context.Context) { + go func() { + // Initialize local copies of authentication providers to allow retry in case of errors + oidcQueue := a.cfg.OpenIDConnect + oauthQueue := a.cfg.OAuth + ldapQueue := a.cfg.Ldap - a.oauthAuthenticators = make(map[string]AuthenticatorOauth, len(a.cfg.OpenIDConnect)+len(a.cfg.OAuth)) - a.ldapAuthenticators = make(map[string]AuthenticatorLdap, len(a.cfg.Ldap)) + ticker := time.NewTicker(30 * time.Second) // Ticker for delay between retries + defer ticker.Stop() - for i := range a.cfg.OpenIDConnect { // OIDC - providerCfg := &a.cfg.OpenIDConnect[i] + for { + select { + case <-ticker.C: + failedOidc, failedOauth, failedLdap := a.setupExternalAuthProviders(oidcQueue, oauthQueue, ldapQueue) + if len(failedOidc) > 0 || len(failedOauth) > 0 || len(failedLdap) > 0 { + slog.Warn("failed to setup some external auth providers, retrying in 30 seconds", + "failedOidc", len(failedOidc), "failedOauth", len(failedOauth), "failedLdap", len(failedLdap)) + // Retry failed providers + oidcQueue = failedOidc + oauthQueue = failedOauth + ldapQueue = failedLdap + } else { + slog.Info("successfully setup all external auth providers") + return // Exit goroutine if all providers are set up successfully + } + case <-ctx.Done(): + slog.Info("context cancelled, stopping setup of external auth providers") + return // Exit goroutine if context is cancelled + } + } + }() +} + +func (a *Authenticator) setupExternalAuthProviders( + oidc []config.OpenIDConnectProvider, + oauth []config.OAuthProvider, + ldap []config.LdapProvider, +) ( + []config.OpenIDConnectProvider, + []config.OAuthProvider, + []config.LdapProvider, +) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var failedOidc []config.OpenIDConnectProvider + var failedOauth []config.OAuthProvider + var failedLdap []config.LdapProvider + + for i := range oidc { // OIDC + providerCfg := &oidc[i] providerId := strings.ToLower(providerCfg.ProviderName) if _, exists := a.oauthAuthenticators[providerId]; exists { - return fmt.Errorf("auth provider with name %s is already registerd", providerId) + // this is an unrecoverable error, we cannot register the same provider twice + slog.Error("OIDC auth provider is already registered", "name", providerId) + continue // skip this provider } - redirectUrl := *extUrl + redirectUrl := *a.callbackUrl redirectUrl.Path = path.Join(redirectUrl.Path, "/auth/login/", providerId, "/callback") provider, err := newOidcAuthenticator(ctx, redirectUrl.String(), providerCfg) if err != nil { - return fmt.Errorf("failed to setup oidc authentication provider %s: %w", providerCfg.ProviderName, err) + failedOidc = append(failedOidc, oidc[i]) + slog.Error("failed to setup oidc authentication provider", "name", providerId, "error", err) + continue } a.oauthAuthenticators[providerId] = provider } - for i := range a.cfg.OAuth { // PLAIN OAUTH - providerCfg := &a.cfg.OAuth[i] + for i := range oauth { // PLAIN OAUTH + providerCfg := &oauth[i] providerId := strings.ToLower(providerCfg.ProviderName) if _, exists := a.oauthAuthenticators[providerId]; exists { - return fmt.Errorf("auth provider with name %s is already registerd", providerId) + // this is an unrecoverable error, we cannot register the same provider twice + slog.Error("OAUTH auth provider is already registered", "name", providerId) + continue // skip this provider } - redirectUrl := *extUrl + redirectUrl := *a.callbackUrl redirectUrl.Path = path.Join(redirectUrl.Path, "/auth/login/", providerId, "/callback") provider, err := newPlainOauthAuthenticator(ctx, redirectUrl.String(), providerCfg) if err != nil { - return fmt.Errorf("failed to setup oauth authentication provider %s: %w", providerId, err) + failedOauth = append(failedOauth, oauth[i]) + slog.Error("failed to setup oauth authentication provider", "name", providerId, "error", err) + continue } a.oauthAuthenticators[providerId] = provider } - for i := range a.cfg.Ldap { // LDAP - providerCfg := &a.cfg.Ldap[i] + for i := range ldap { // LDAP + providerCfg := &ldap[i] providerId := strings.ToLower(providerCfg.URL) if _, exists := a.ldapAuthenticators[providerId]; exists { - return fmt.Errorf("auth provider with name %s is already registerd", providerId) + // this is an unrecoverable error, we cannot register the same provider twice + slog.Error("LDAP auth provider is already registered", "name", providerId) + continue // skip this provider } provider, err := newLdapAuthenticator(ctx, providerCfg) if err != nil { - return fmt.Errorf("failed to setup ldap authentication provider %s: %w", providerId, err) + failedLdap = append(failedLdap, ldap[i]) + slog.Error("failed to setup ldap authentication provider", "name", providerId, "error", err) + continue } a.ldapAuthenticators[providerId] = provider } - return nil + return failedOidc, failedOauth, failedLdap } // GetExternalLoginProviders returns a list of all available external login providers. @@ -434,6 +490,10 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, return nil, fmt.Errorf("unable to parse user information: %w", err) } + if !isDomainAllowed(userInfo.Email, oauthProvider.GetAllowedDomains()) { + return nil, fmt.Errorf("user %s is not in allowed domains", userInfo.Email) + } + ctx = domain.SetUserInfo(ctx, domain.SystemAdminContextUserInfo()) // switch to admin user context to check if user exists user, err := a.processUserInfo(ctx, userInfo, domain.UserSourceOauth, oauthProvider.GetName(), @@ -450,10 +510,6 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, 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() { a.bus.Publish(app.TopicAuditLoginFailed, domain.AuditEventWrapper[audit.AuthEvent]{ Ctx: ctx, diff --git a/internal/app/configfile/manager.go b/internal/app/configfile/manager.go index 83c07f4..94f06ae 100644 --- a/internal/app/configfile/manager.go +++ b/internal/app/configfile/manager.go @@ -46,7 +46,7 @@ type TemplateRenderer interface { // GetInterfaceConfig returns the configuration file for the given interface. GetInterfaceConfig(iface *domain.Interface, peers []domain.Peer) (io.Reader, error) // GetPeerConfig returns the configuration file for the given peer. - GetPeerConfig(peer *domain.Peer) (io.Reader, error) + GetPeerConfig(peer *domain.Peer, style string) (io.Reader, error) } type EventBus interface { @@ -186,7 +186,7 @@ func (m Manager) GetInterfaceConfig(ctx context.Context, id domain.InterfaceIden // GetPeerConfig returns the configuration file for the given peer. // The file is structured in wg-quick format. -func (m Manager) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) { +func (m Manager) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) { peer, err := m.wg.GetPeer(ctx, id) if err != nil { return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err) @@ -196,11 +196,11 @@ func (m Manager) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (i return nil, err } - return m.tplHandler.GetPeerConfig(peer) + return m.tplHandler.GetPeerConfig(peer, style) } // GetPeerConfigQrCode returns a QR code image containing the configuration for the given peer. -func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) { +func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) { peer, err := m.wg.GetPeer(ctx, id) if err != nil { return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err) @@ -210,7 +210,7 @@ func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifi return nil, err } - cfgData, err := m.tplHandler.GetPeerConfig(peer) + cfgData, err := m.tplHandler.GetPeerConfig(peer, style) if err != nil { return nil, fmt.Errorf("failed to get peer config for %s: %w", id, err) } diff --git a/internal/app/configfile/template.go b/internal/app/configfile/template.go index ed40d9b..773ab8c 100644 --- a/internal/app/configfile/template.go +++ b/internal/app/configfile/template.go @@ -55,11 +55,12 @@ func (c TemplateHandler) GetInterfaceConfig(cfg *domain.Interface, peers []domai } // GetPeerConfig returns the rendered configuration file for a WireGuard peer. -func (c TemplateHandler) GetPeerConfig(peer *domain.Peer) (io.Reader, error) { +func (c TemplateHandler) GetPeerConfig(peer *domain.Peer, style string) (io.Reader, error) { var tplBuff bytes.Buffer err := c.templates.ExecuteTemplate(&tplBuff, "wg_peer.tpl", map[string]any{ - "Peer": peer, + "Style": style, + "Peer": peer, "Portal": map[string]any{ "Version": "unknown", }, diff --git a/internal/app/configfile/tpl_files/wg_peer.tpl b/internal/app/configfile/tpl_files/wg_peer.tpl index 8a4bae4..211546b 100644 --- a/internal/app/configfile/tpl_files/wg_peer.tpl +++ b/internal/app/configfile/tpl_files/wg_peer.tpl @@ -1,6 +1,8 @@ # AUTOGENERATED FILE - DO NOT EDIT -# This file uses wg-quick format. +# This file uses {{ .Style }} format. +{{- if eq .Style "wgquick"}} # See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION +{{- end}} # Lines starting with the -WGP- tag are used by # the WireGuard Portal configuration parser. @@ -21,22 +23,27 @@ # Core settings PrivateKey = {{ .Peer.Interface.KeyPair.PrivateKey }} +{{- if eq .Style "wgquick"}} Address = {{ CidrsToString .Peer.Interface.Addresses }} +{{- end}} # Misc. settings (optional) +{{- if eq .Style "wgquick"}} {{- if .Peer.Interface.DnsStr.GetValue}} DNS = {{ .Peer.Interface.DnsStr.GetValue }} {{- if .Peer.Interface.DnsSearchStr.GetValue}}, {{ .Peer.Interface.DnsSearchStr.GetValue }} {{- end}} {{- end}} {{- if ne .Peer.Interface.Mtu.GetValue 0}} MTU = {{ .Peer.Interface.Mtu.GetValue }} {{- end}} -{{- if ne .Peer.Interface.FirewallMark.GetValue 0}} -FwMark = {{ .Peer.Interface.FirewallMark.GetValue }} -{{- end}} {{- if ne .Peer.Interface.RoutingTable.GetValue ""}} Table = {{ .Peer.Interface.RoutingTable.GetValue }} {{- end}} +{{- end}} +{{- if ne .Peer.Interface.FirewallMark.GetValue 0}} +FwMark = {{ .Peer.Interface.FirewallMark.GetValue }} +{{- end}} +{{- if eq .Style "wgquick"}} # Interface hooks (optional) {{- if .Peer.Interface.PreUp.GetValue}} PreUp = {{ .Peer.Interface.PreUp.GetValue }} @@ -50,6 +57,7 @@ PreDown = {{ .Peer.Interface.PreDown.GetValue }} {{- if .Peer.Interface.PostDown.GetValue}} PostDown = {{ .Peer.Interface.PostDown.GetValue }} {{- end}} +{{- end}} [Peer] PublicKey = {{ .Peer.EndpointPublicKey.GetValue }} diff --git a/internal/app/eventbus.go b/internal/app/eventbus.go index 56a3220..d411aa6 100644 --- a/internal/app/eventbus.go +++ b/internal/app/eventbus.go @@ -36,6 +36,7 @@ const TopicPeerDeleted = "peer:deleted" const TopicPeerUpdated = "peer:updated" const TopicPeerInterfaceUpdated = "peer:interface:updated" const TopicPeerIdentifierUpdated = "peer:identifier:updated" +const TopicPeerStateChanged = "peer:state:changed" // endregion peer-events diff --git a/internal/app/mail/manager.go b/internal/app/mail/manager.go index 7b31406..7b9b3b9 100644 --- a/internal/app/mail/manager.go +++ b/internal/app/mail/manager.go @@ -21,9 +21,9 @@ type ConfigFileManager interface { // GetInterfaceConfig returns the configuration for the given interface. GetInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) (io.Reader, error) // GetPeerConfig returns the configuration for the given peer. - GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) + GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) // GetPeerConfigQrCode returns the QR code for the given peer. - GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) + GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) } type UserDatabaseRepo interface { @@ -71,7 +71,7 @@ func NewMailManager( users UserDatabaseRepo, wg WireguardDatabaseRepo, ) (*Manager, error) { - tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl) + tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl, cfg.Web.SiteTitle) if err != nil { return nil, fmt.Errorf("failed to initialize template handler: %w", err) } @@ -89,7 +89,7 @@ func NewMailManager( } // SendPeerEmail sends an email to the user linked to the given peers. -func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error { +func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error { for _, peerId := range peers { peer, err := m.wg.GetPeer(ctx, peerId) if err != nil { @@ -123,7 +123,7 @@ func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...doma continue } - err = m.sendPeerEmail(ctx, linkOnly, user, peer) + err = m.sendPeerEmail(ctx, linkOnly, style, user, peer) if err != nil { return fmt.Errorf("failed to send peer email for %s: %w", peerId, err) } @@ -132,7 +132,13 @@ func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...doma return nil } -func (m Manager) sendPeerEmail(ctx context.Context, linkOnly bool, user *domain.User, peer *domain.Peer) error { +func (m Manager) sendPeerEmail( + ctx context.Context, + linkOnly bool, + style string, + user *domain.User, + peer *domain.Peer, +) error { qrName := "WireGuardQRCode.png" configName := peer.GetConfigFileName() @@ -148,12 +154,12 @@ func (m Manager) sendPeerEmail(ctx context.Context, linkOnly bool, user *domain. } } else { - peerConfig, err := m.configFiles.GetPeerConfig(ctx, peer.Identifier) + peerConfig, err := m.configFiles.GetPeerConfig(ctx, peer.Identifier, style) if err != nil { return fmt.Errorf("failed to fetch peer config for %s: %w", peer.Identifier, err) } - peerConfigQr, err := m.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier) + peerConfigQr, err := m.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier, style) if err != nil { return fmt.Errorf("failed to fetch peer config QR code for %s: %w", peer.Identifier, err) } diff --git a/internal/app/mail/template.go b/internal/app/mail/template.go index 15253bf..722d534 100644 --- a/internal/app/mail/template.go +++ b/internal/app/mail/template.go @@ -17,11 +17,12 @@ var TemplateFiles embed.FS // TemplateHandler is a struct that holds the html and text templates. type TemplateHandler struct { portalUrl string + portalName string htmlTemplates *htmlTemplate.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") if err != nil { return nil, fmt.Errorf("failed to parse html template files: %w", err) @@ -34,6 +35,7 @@ func newTemplateHandler(portalUrl string) (*TemplateHandler, error) { handler := &TemplateHandler{ portalUrl: portalUrl, + portalName: portalName, htmlTemplates: htmlTemplateCache, textTemplates: txtTemplateCache, } @@ -81,6 +83,7 @@ func (c TemplateHandler) GetConfigMailWithAttachment(user *domain.User, cfgName, "ConfigFileName": cfgName, "QrcodePngName": qrName, "PortalUrl": c.portalUrl, + "PortalName": c.portalName, }) if err != nil { 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, "QrcodePngName": qrName, "PortalUrl": c.portalUrl, + "PortalName": c.portalName, }) if err != nil { return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gohtml: %w", err) diff --git a/internal/app/mail/tpl_files/mail_with_attachment.gohtml b/internal/app/mail/tpl_files/mail_with_attachment.gohtml index 0243848..0bc5d70 100644 --- a/internal/app/mail/tpl_files/mail_with_attachment.gohtml +++ b/internal/app/mail/tpl_files/mail_with_attachment.gohtml @@ -19,7 +19,7 @@ - Email Template + {{$.PortalName}} - Email Template + {{$.PortalName}}