From edb88b5768d25a4cce1d352a05ea6d02f606c19f Mon Sep 17 00:00:00 2001 From: h44z Date: Sun, 29 Jun 2025 19:49:01 +0200 Subject: [PATCH] new webhook models (#444) (#471) warning: existing webhook receivers need to be adapted to the new models --- docs/documentation/configuration/overview.md | 13 - docs/documentation/usage/webhooks.md | 237 +++++++++++++++++-- internal/app/webhooks/manager.go | 43 ++-- internal/app/webhooks/model.go | 7 +- internal/app/webhooks/models/interface.go | 99 ++++++++ internal/app/webhooks/models/peer.go | 89 +++++++ internal/app/webhooks/models/peer_metrics.go | 50 ++++ internal/app/webhooks/models/user.go | 56 +++++ internal/app/wireguard/statistics.go | 10 +- 9 files changed, 546 insertions(+), 58 deletions(-) create mode 100644 internal/app/webhooks/models/interface.go create mode 100644 internal/app/webhooks/models/peer.go create mode 100644 internal/app/webhooks/models/peer_metrics.go create mode 100644 internal/app/webhooks/models/user.go diff --git a/docs/documentation/configuration/overview.md b/docs/documentation/configuration/overview.md index 3611171..dd20d79 100644 --- a/docs/documentation/configuration/overview.md +++ b/docs/documentation/configuration/overview.md @@ -673,19 +673,6 @@ 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": "update", - "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` diff --git a/docs/documentation/usage/webhooks.md b/docs/documentation/usage/webhooks.md index 7ec8cbf..1d0c692 100644 --- a/docs/documentation/usage/webhooks.md +++ b/docs/documentation/usage/webhooks.md @@ -38,11 +38,12 @@ WireGuard Portal supports various events that can trigger webhooks. The followin - `connect`: Triggered when a user connects to the VPN. - `disconnect`: Triggered when a user disconnects from the VPN. -The following entity types can trigger webhooks: +The following entity models are supported for webhook events: -- `user`: When a WireGuard Portal user is created, updated, or deleted. -- `peer`: When a peer is created, updated, or deleted. This entity can also trigger `connect` and `disconnect` events. -- `interface`: When a device is created, updated, or deleted. +- `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 @@ -51,36 +52,234 @@ A common shell structure for webhook payloads is as follows: ```json { - "event": "create", - "entity": "user", - "identifier": "the-user-identifier", + "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. peer data. - // Check the API documentation for the exact structure. + // The payload of the event, e.g. a Peer model. + // Detailed model descriptions are provided below. } } ``` +### Payload Models -### Example Payload +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": { - "PeerId": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", - "IsConnected": true, - "IsPingable": false, - "LastPing": null, - "BytesReceived": 1860, - "BytesTransmitted": 10824, - "LastHandshake": "2025-06-26T23:04:33.325216659+02:00", - "Endpoint": "10.55.66.77:33874", - "LastSessionStart": "2025-06-26T22:50:40.10221606+02:00" + "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/internal/app/webhooks/manager.go b/internal/app/webhooks/manager.go index 702ac6c..b8d010c 100644 --- a/internal/app/webhooks/manager.go +++ b/internal/app/webhooks/manager.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/h44z/wg-portal/internal/app" + "github.com/h44z/wg-portal/internal/app/webhooks/models" "github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/domain" ) @@ -101,46 +102,46 @@ func (m Manager) sendWebhook(ctx context.Context, data io.Reader) error { } func (m Manager) handleUserCreateEvent(user domain.User) { - m.handleGenericEvent(WebhookEventCreate, user) + m.handleGenericEvent(WebhookEventCreate, models.NewUser(user)) } func (m Manager) handleUserUpdateEvent(user domain.User) { - m.handleGenericEvent(WebhookEventUpdate, user) + m.handleGenericEvent(WebhookEventUpdate, models.NewUser(user)) } func (m Manager) handleUserDeleteEvent(user domain.User) { - m.handleGenericEvent(WebhookEventDelete, user) + m.handleGenericEvent(WebhookEventDelete, models.NewUser(user)) } func (m Manager) handlePeerCreateEvent(peer domain.Peer) { - m.handleGenericEvent(WebhookEventCreate, peer) + m.handleGenericEvent(WebhookEventCreate, models.NewPeer(peer)) } func (m Manager) handlePeerUpdateEvent(peer domain.Peer) { - m.handleGenericEvent(WebhookEventUpdate, peer) + m.handleGenericEvent(WebhookEventUpdate, models.NewPeer(peer)) } func (m Manager) handlePeerDeleteEvent(peer domain.Peer) { - m.handleGenericEvent(WebhookEventDelete, peer) + m.handleGenericEvent(WebhookEventDelete, models.NewPeer(peer)) } func (m Manager) handleInterfaceCreateEvent(iface domain.Interface) { - m.handleGenericEvent(WebhookEventCreate, iface) + m.handleGenericEvent(WebhookEventCreate, models.NewInterface(iface)) } func (m Manager) handleInterfaceUpdateEvent(iface domain.Interface) { - m.handleGenericEvent(WebhookEventUpdate, iface) + m.handleGenericEvent(WebhookEventUpdate, models.NewInterface(iface)) } func (m Manager) handleInterfaceDeleteEvent(iface domain.Interface) { - m.handleGenericEvent(WebhookEventDelete, iface) + m.handleGenericEvent(WebhookEventDelete, models.NewInterface(iface)) } -func (m Manager) handlePeerStateChangeEvent(peerStatus domain.PeerStatus) { +func (m Manager) handlePeerStateChangeEvent(peerStatus domain.PeerStatus, peer domain.Peer) { if peerStatus.IsConnected { - m.handleGenericEvent(WebhookEventConnect, peerStatus) + m.handleGenericEvent(WebhookEventConnect, models.NewPeerMetrics(peerStatus, peer)) } else { - m.handleGenericEvent(WebhookEventDisconnect, peerStatus) + m.handleGenericEvent(WebhookEventDisconnect, models.NewPeerMetrics(peerStatus, peer)) } } @@ -177,18 +178,18 @@ func (m Manager) createWebhookData(action WebhookEvent, payload any) (*WebhookDa } switch v := payload.(type) { - case domain.User: + case models.User: d.Entity = WebhookEntityUser - d.Identifier = string(v.Identifier) - case domain.Peer: + d.Identifier = v.Identifier + case models.Peer: d.Entity = WebhookEntityPeer - d.Identifier = string(v.Identifier) - case domain.Interface: + d.Identifier = v.Identifier + case models.Interface: d.Entity = WebhookEntityInterface - d.Identifier = string(v.Identifier) - case domain.PeerStatus: - d.Entity = WebhookEntityPeer - d.Identifier = string(v.PeerId) + d.Identifier = v.Identifier + case models.PeerMetrics: + d.Entity = WebhookEntityPeerMetric + d.Identifier = v.Peer.Identifier default: return nil, fmt.Errorf("unsupported payload type: %T", v) } diff --git a/internal/app/webhooks/model.go b/internal/app/webhooks/model.go index b0ca9a5..c806d47 100644 --- a/internal/app/webhooks/model.go +++ b/internal/app/webhooks/model.go @@ -34,9 +34,10 @@ func (d *WebhookData) Serialize() (io.Reader, error) { type WebhookEntity = string const ( - WebhookEntityUser WebhookEntity = "user" - WebhookEntityPeer WebhookEntity = "peer" - WebhookEntityInterface WebhookEntity = "interface" + WebhookEntityUser WebhookEntity = "user" + WebhookEntityPeer WebhookEntity = "peer" + WebhookEntityPeerMetric WebhookEntity = "peer_metric" + WebhookEntityInterface WebhookEntity = "interface" ) type WebhookEvent = string diff --git a/internal/app/webhooks/models/interface.go b/internal/app/webhooks/models/interface.go new file mode 100644 index 0000000..c3a0eea --- /dev/null +++ b/internal/app/webhooks/models/interface.go @@ -0,0 +1,99 @@ +package models + +import ( + "time" + + "github.com/h44z/wg-portal/internal/domain" +) + +// Interface represents an interface model for webhooks. For details about the fields, see the domain.Interface struct. +type Interface struct { + CreatedBy string `json:"CreatedBy"` + UpdatedBy string `json:"UpdatedBy"` + CreatedAt time.Time `json:"CreatedAt"` + UpdatedAt time.Time `json:"UpdatedAt"` + + Identifier string `json:"Identifier"` + PrivateKey string `json:"PrivateKey"` + PublicKey string `json:"PublicKey"` + ListenPort int `json:"ListenPort"` + + Addresses []string `json:"Addresses"` + DnsStr string `json:"DnsStr"` + DnsSearchStr string `json:"DnsSearchStr"` + + Mtu int `json:"Mtu"` + FirewallMark uint32 `json:"FirewallMark"` + RoutingTable string `json:"RoutingTable"` + + PreUp string `json:"PreUp"` + PostUp string `json:"PostUp"` + PreDown string `json:"PreDown"` + PostDown string `json:"PostDown"` + + SaveConfig bool `json:"SaveConfig"` + + DisplayName string `json:"DisplayName"` + Type string `json:"Type"` + DriverType string `json:"DriverType"` + Disabled *time.Time `json:"Disabled,omitempty"` + DisabledReason string `json:"DisabledReason,omitempty"` + + PeerDefNetworkStr string `json:"PeerDefNetworkStr,omitempty"` + PeerDefDnsStr string `json:"PeerDefDnsStr,omitempty"` + PeerDefDnsSearchStr string `json:"PeerDefDnsSearchStr,omitempty"` + PeerDefEndpoint string `json:"PeerDefEndpoint,omitempty"` + PeerDefAllowedIPsStr string `json:"PeerDefAllowedIPsStr,omitempty"` + PeerDefMtu int `json:"PeerDefMtu,omitempty"` + PeerDefPersistentKeepalive int `json:"PeerDefPersistentKeepalive,omitempty"` + PeerDefFirewallMark uint32 `json:"PeerDefFirewallMark,omitempty"` + PeerDefRoutingTable string `json:"PeerDefRoutingTable,omitempty"` + + PeerDefPreUp string `json:"PeerDefPreUp,omitempty"` + PeerDefPostUp string `json:"PeerDefPostUp,omitempty"` + PeerDefPreDown string `json:"PeerDefPreDown,omitempty"` + PeerDefPostDown string `json:"PeerDefPostDown,omitempty"` +} + +// NewInterface creates a new Interface model from a domain.Interface. +func NewInterface(src domain.Interface) Interface { + return Interface{ + CreatedBy: src.CreatedBy, + UpdatedBy: src.UpdatedBy, + CreatedAt: src.CreatedAt, + UpdatedAt: src.UpdatedAt, + Identifier: string(src.Identifier), + PrivateKey: src.KeyPair.PrivateKey, + PublicKey: src.KeyPair.PublicKey, + ListenPort: src.ListenPort, + Addresses: domain.CidrsToStringSlice(src.Addresses), + DnsStr: src.DnsStr, + DnsSearchStr: src.DnsSearchStr, + Mtu: src.Mtu, + FirewallMark: src.FirewallMark, + RoutingTable: src.RoutingTable, + PreUp: src.PreUp, + PostUp: src.PostUp, + PreDown: src.PreDown, + PostDown: src.PostDown, + SaveConfig: src.SaveConfig, + DisplayName: string(src.Identifier), + Type: string(src.Type), + DriverType: src.DriverType, + Disabled: src.Disabled, + DisabledReason: src.DisabledReason, + PeerDefNetworkStr: src.PeerDefNetworkStr, + PeerDefDnsStr: src.PeerDefDnsStr, + PeerDefDnsSearchStr: src.PeerDefDnsSearchStr, + PeerDefEndpoint: src.PeerDefEndpoint, + PeerDefAllowedIPsStr: src.PeerDefAllowedIPsStr, + PeerDefMtu: src.PeerDefMtu, + PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive, + PeerDefFirewallMark: src.PeerDefFirewallMark, + PeerDefRoutingTable: src.PeerDefRoutingTable, + PeerDefPreUp: src.PeerDefPreUp, + PeerDefPostUp: src.PeerDefPostUp, + PeerDefPreDown: src.PeerDefPreDown, + PeerDefPostDown: src.PeerDefPostDown, + } +} diff --git a/internal/app/webhooks/models/peer.go b/internal/app/webhooks/models/peer.go new file mode 100644 index 0000000..c7a5919 --- /dev/null +++ b/internal/app/webhooks/models/peer.go @@ -0,0 +1,89 @@ +package models + +import ( + "time" + + "github.com/h44z/wg-portal/internal/domain" +) + +// Peer represents a peer model for webhooks. For details about the fields, see the domain.Peer struct. +type Peer struct { + CreatedBy string `json:"CreatedBy"` + UpdatedBy string `json:"UpdatedBy"` + CreatedAt time.Time `json:"CreatedAt"` + UpdatedAt time.Time `json:"UpdatedAt"` + + Endpoint string `json:"Endpoint"` + EndpointPublicKey string `json:"EndpointPublicKey"` + AllowedIPsStr string `json:"AllowedIPsStr"` + ExtraAllowedIPsStr string `json:"ExtraAllowedIPsStr"` + PresharedKey string `json:"PresharedKey"` + PersistentKeepalive int `json:"PersistentKeepalive"` + + DisplayName string `json:"DisplayName"` + Identifier string `json:"Identifier"` + UserIdentifier string `json:"UserIdentifier"` + InterfaceIdentifier string `json:"InterfaceIdentifier"` + Disabled *time.Time `json:"Disabled,omitempty"` + DisabledReason string `json:"DisabledReason,omitempty"` + ExpiresAt *time.Time `json:"ExpiresAt,omitempty"` + Notes string `json:"Notes,omitempty"` + AutomaticallyCreated bool `json:"AutomaticallyCreated"` + + PrivateKey string `json:"PrivateKey"` + PublicKey string `json:"PublicKey"` + + InterfaceType string `json:"InterfaceType"` + + Addresses []string `json:"Addresses"` + CheckAliveAddress string `json:"CheckAliveAddress"` + DnsStr string `json:"DnsStr"` + DnsSearchStr string `json:"DnsSearchStr"` + Mtu int `json:"Mtu"` + FirewallMark uint32 `json:"FirewallMark,omitempty"` + RoutingTable string `json:"RoutingTable,omitempty"` + + PreUp string `json:"PreUp,omitempty"` + PostUp string `json:"PostUp,omitempty"` + PreDown string `json:"PreDown,omitempty"` + PostDown string `json:"PostDown,omitempty"` +} + +// NewPeer creates a new Peer model from a domain.Peer. +func NewPeer(src domain.Peer) Peer { + return Peer{ + CreatedBy: src.CreatedBy, + UpdatedBy: src.UpdatedBy, + CreatedAt: src.CreatedAt, + UpdatedAt: src.UpdatedAt, + Endpoint: src.Endpoint.GetValue(), + EndpointPublicKey: src.EndpointPublicKey.GetValue(), + AllowedIPsStr: src.AllowedIPsStr.GetValue(), + ExtraAllowedIPsStr: src.ExtraAllowedIPsStr, + PresharedKey: string(src.PresharedKey), + PersistentKeepalive: src.PersistentKeepalive.GetValue(), + DisplayName: src.DisplayName, + Identifier: string(src.Identifier), + UserIdentifier: string(src.UserIdentifier), + InterfaceIdentifier: string(src.InterfaceIdentifier), + Disabled: src.Disabled, + DisabledReason: src.DisabledReason, + ExpiresAt: src.ExpiresAt, + Notes: src.Notes, + AutomaticallyCreated: src.AutomaticallyCreated, + PrivateKey: src.Interface.KeyPair.PrivateKey, + PublicKey: src.Interface.KeyPair.PublicKey, + InterfaceType: string(src.Interface.Type), + Addresses: domain.CidrsToStringSlice(src.Interface.Addresses), + CheckAliveAddress: src.Interface.CheckAliveAddress, + DnsStr: src.Interface.DnsStr.GetValue(), + DnsSearchStr: src.Interface.DnsSearchStr.GetValue(), + Mtu: src.Interface.Mtu.GetValue(), + FirewallMark: src.Interface.FirewallMark.GetValue(), + RoutingTable: src.Interface.RoutingTable.GetValue(), + PreUp: src.Interface.PreUp.GetValue(), + PostUp: src.Interface.PostUp.GetValue(), + PreDown: src.Interface.PreDown.GetValue(), + PostDown: src.Interface.PostDown.GetValue(), + } +} diff --git a/internal/app/webhooks/models/peer_metrics.go b/internal/app/webhooks/models/peer_metrics.go new file mode 100644 index 0000000..f380d2c --- /dev/null +++ b/internal/app/webhooks/models/peer_metrics.go @@ -0,0 +1,50 @@ +package models + +import ( + "time" + + "github.com/h44z/wg-portal/internal/domain" +) + +// PeerMetrics represents a peer metrics model for webhooks. +// For details about the fields, see the domain.PeerStatus and domain.Peer structs. +type PeerMetrics struct { + Status PeerStatus `json:"Status"` + Peer Peer `json:"Peer"` +} + +// PeerStatus represents the status of a peer for webhooks. +// For details about the fields, see the domain.PeerStatus struct. +type PeerStatus struct { + UpdatedAt time.Time `json:"UpdatedAt"` + + IsConnected bool `json:"IsConnected"` + + IsPingable bool `json:"IsPingable"` + LastPing *time.Time `json:"LastPing,omitempty"` + + BytesReceived uint64 `json:"BytesReceived"` + BytesTransmitted uint64 `json:"BytesTransmitted"` + + Endpoint string `json:"Endpoint"` + LastHandshake *time.Time `json:"LastHandshake,omitempty"` + LastSessionStart *time.Time `json:"LastSessionStart,omitempty"` +} + +// NewPeerMetrics creates a new PeerMetrics model from the domain.PeerStatus and domain.Peer models. +func NewPeerMetrics(status domain.PeerStatus, peer domain.Peer) PeerMetrics { + return PeerMetrics{ + Status: PeerStatus{ + UpdatedAt: status.UpdatedAt, + IsConnected: status.IsConnected, + IsPingable: status.IsPingable, + LastPing: status.LastPing, + BytesReceived: status.BytesReceived, + BytesTransmitted: status.BytesTransmitted, + Endpoint: status.Endpoint, + LastHandshake: status.LastHandshake, + LastSessionStart: status.LastSessionStart, + }, + Peer: NewPeer(peer), + } +} diff --git a/internal/app/webhooks/models/user.go b/internal/app/webhooks/models/user.go new file mode 100644 index 0000000..defe962 --- /dev/null +++ b/internal/app/webhooks/models/user.go @@ -0,0 +1,56 @@ +package models + +import ( + "time" + + "github.com/h44z/wg-portal/internal/domain" +) + +// User represents a user model for webhooks. For details about the fields, see the domain.User struct. +type User struct { + CreatedBy string `json:"CreatedBy"` + UpdatedBy string `json:"UpdatedBy"` + CreatedAt time.Time `json:"CreatedAt"` + UpdatedAt time.Time `json:"UpdatedAt"` + + Identifier string `json:"Identifier"` + Email string `json:"Email"` + Source string `json:"Source"` + ProviderName string `json:"ProviderName"` + IsAdmin bool `json:"IsAdmin"` + + Firstname string `json:"Firstname,omitempty"` + Lastname string `json:"Lastname,omitempty"` + Phone string `json:"Phone,omitempty"` + Department string `json:"Department,omitempty"` + Notes string `json:"Notes,omitempty"` + + Disabled *time.Time `json:"Disabled,omitempty"` + DisabledReason string `json:"DisabledReason,omitempty"` + Locked *time.Time `json:"Locked,omitempty"` + LockedReason string `json:"LockedReason,omitempty"` +} + +// NewUser creates a new User model from a domain.User +func NewUser(src domain.User) User { + return User{ + CreatedBy: src.CreatedBy, + UpdatedBy: src.UpdatedBy, + CreatedAt: src.CreatedAt, + UpdatedAt: src.UpdatedAt, + Identifier: string(src.Identifier), + Email: src.Email, + Source: string(src.Source), + ProviderName: src.ProviderName, + IsAdmin: src.IsAdmin, + Firstname: src.Firstname, + Lastname: src.Lastname, + Phone: src.Phone, + Department: src.Department, + Notes: src.Notes, + Disabled: src.Disabled, + DisabledReason: src.DisabledReason, + Locked: src.Locked, + LockedReason: src.LockedReason, + } +} diff --git a/internal/app/wireguard/statistics.go b/internal/app/wireguard/statistics.go index 2733468..dcc8b36 100644 --- a/internal/app/wireguard/statistics.go +++ b/internal/app/wireguard/statistics.go @@ -213,8 +213,14 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) { } if connectionStateChanged { + peerModel, err := c.db.GetPeer(ctx, peer.Identifier) + if err != nil { + slog.Error("failed to fetch peer for data collection", "peer", peer.Identifier, "error", + err) + continue + } // publish event if connection state changed - c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus) + c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus, *peerModel) } } } @@ -356,7 +362,7 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) { if connectionStateChanged { // publish event if connection state changed - c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus) + c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus, peer) } } }