Compare commits

..

1 Commits

Author SHA1 Message Date
Christoph Haas
9c1e037166 new webhook models (#444)
warning: existing webhook receivers need to be adapted to the new models
2025-06-27 22:28:33 +02:00
23 changed files with 597 additions and 207 deletions

View File

@@ -673,19 +673,6 @@ Without a valid `external_url`, the login process may fail due to CSRF protectio
## Webhook ## Webhook
The webhook section allows you to configure a webhook that is called on certain events in WireGuard Portal. 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). Further details can be found in the [usage documentation](../usage/webhooks.md).
### `url` ### `url`

View File

@@ -38,11 +38,12 @@ WireGuard Portal supports various events that can trigger webhooks. The followin
- `connect`: Triggered when a user connects to the VPN. - `connect`: Triggered when a user connects to the VPN.
- `disconnect`: Triggered when a user disconnects from 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. - `user`: WireGuard Portal users support creation, update, or deletion events.
- `peer`: When a peer is created, updated, or deleted. This entity can also trigger `connect` and `disconnect` events. - `peer`: Peers support creation, update, or deletion events. Via the `peer_metric` entity, you can also receive connection status updates.
- `interface`: When a device is created, updated, or deleted. - `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 ## Payload Structure
@@ -51,36 +52,234 @@ A common shell structure for webhook payloads is as follows:
```json ```json
{ {
"event": "create", "event": "create", // The event type, e.g. "create", "update", "delete", "connect", "disconnect"
"entity": "user", "entity": "user", // The entity type, e.g. "user", "peer", "peer_metric", "interface"
"identifier": "the-user-identifier", "identifier": "the-user-identifier", // Unique identifier of the entity, e.g. user ID or peer ID
"payload": { "payload": {
// The payload of the event, e.g. peer data. // The payload of the event, e.g. a Peer model.
// Check the API documentation for the exact structure. // 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: The following payload is an example of a webhook event when a peer connects to the VPN:
```json ```json
{ {
"event": "connect", "event": "connect",
"entity": "peer", "entity": "peer_metric",
"identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", "identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
"payload": { "payload": {
"PeerId": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", "Status": {
"IsConnected": true, "UpdatedAt": "2025-06-27T22:20:08.734900034+02:00",
"IsPingable": false, "IsConnected": true,
"LastPing": null, "IsPingable": false,
"BytesReceived": 1860, "BytesReceived": 212,
"BytesTransmitted": 10824, "BytesTransmitted": 2884,
"LastHandshake": "2025-06-26T23:04:33.325216659+02:00", "Endpoint": "10.55.66.77:58756",
"Endpoint": "10.55.66.77:33874", "LastHandshake": "2025-06-27T22:19:46.580842776+02:00",
"LastSessionStart": "2025-06-26T22:50:40.10221606+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
} }
} }
``` ```

View File

@@ -50,7 +50,7 @@ const selectedStats = computed(() => {
if (!s) { if (!s) {
if (!!props.peerId || props.peerId.length) { if (!!props.peerId || props.peerId.length) {
s = profile.Statistics(props.peerId) p = profile.Statistics(props.peerId)
} else { } else {
s = freshStats() // dummy stats to avoid 'undefined' exceptions s = freshStats() // dummy stats to avoid 'undefined' exceptions
} }
@@ -79,19 +79,13 @@ const title = computed(() => {
} }
}) })
const configStyle = ref("wgquick")
watch(() => props.visible, async (newValue, oldValue) => { watch(() => props.visible, async (newValue, oldValue) => {
if (oldValue === false && newValue === true) { // if modal is shown if (oldValue === false && newValue === true) { // if modal is shown
await peers.LoadPeerConfig(selectedPeer.value.Identifier, configStyle.value) await peers.LoadPeerConfig(selectedPeer.value.Identifier)
configString.value = peers.configuration configString.value = peers.configuration
} }
}) }
)
watch(() => configStyle.value, async () => {
await peers.LoadPeerConfig(selectedPeer.value.Identifier, configStyle.value)
configString.value = peers.configuration
})
function download() { function download() {
// credit: https://www.bitdegree.org/learn/javascript-download // credit: https://www.bitdegree.org/learn/javascript-download
@@ -109,7 +103,7 @@ function download() {
} }
function email() { function email() {
peers.MailPeerConfig(settings.Setting("MailLinkOnly"), configStyle.value, [selectedPeer.value.Identifier]).catch(e => { peers.MailPeerConfig(settings.Setting("MailLinkOnly"), [selectedPeer.value.Identifier]).catch(e => {
notify({ notify({
title: "Failed to send mail with peer configuration!", title: "Failed to send mail with peer configuration!",
text: e.toString(), text: e.toString(),
@@ -120,7 +114,7 @@ function email() {
function ConfigQrUrl() { function ConfigQrUrl() {
if (props.peerId.length) { if (props.peerId.length) {
return apiWrapper.url(`/peer/config-qr/${base64_url_encode(props.peerId)}?style=${configStyle.value}`) return apiWrapper.url(`/peer/config-qr/${base64_url_encode(props.peerId)}`)
} }
return '' return ''
} }
@@ -130,15 +124,6 @@ function ConfigQrUrl() {
<template> <template>
<Modal :title="title" :visible="visible" @close="close"> <Modal :title="title" :visible="visible" @close="close">
<template #default> <template #default>
<div class="d-flex justify-content-end align-items-center mb-1">
<span class="me-2">{{ $t('modals.peer-view.style-label') }}: </span>
<div class="btn-group btn-switch-group" role="group" aria-label="Configuration Style">
<input type="radio" class="btn-check" name="configstyle" id="raw" value="raw" autocomplete="off" checked="" v-model="configStyle">
<label class="btn btn-outline-primary btn-sm" for="raw">Raw</label>
<input type="radio" class="btn-check" name="configstyle" id="wgquick" value="wgquick" autocomplete="off" checked="" v-model="configStyle">
<label class="btn btn-outline-primary btn-sm" for="wgquick">WG-Quick</label>
</div>
</div>
<div class="accordion" id="peerInformation"> <div class="accordion" id="peerInformation">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <h2 class="accordion-header">
@@ -228,14 +213,6 @@ function ConfigQrUrl() {
</template> </template>
</Modal></template> </Modal></template>
<style> <style>.config-qr-img {
.config-qr-img {
max-width: 100%; max-width: 100%;
} }</style>
.btn-switch-group .btn {
border-width: 1px;
padding: 5px;
line-height: 1;
}
</style>

View File

@@ -467,8 +467,7 @@
"connected-since": "Verbunden seit", "connected-since": "Verbunden seit",
"endpoint": "Endpunkt", "endpoint": "Endpunkt",
"button-download": "Konfiguration herunterladen", "button-download": "Konfiguration herunterladen",
"button-email": "Konfiguration per E-Mail senden", "button-email": "Konfiguration per E-Mail senden"
"style-label": "Konfigurationsformat"
}, },
"peer-edit": { "peer-edit": {
"headline-edit-peer": "Peer bearbeiten:", "headline-edit-peer": "Peer bearbeiten:",

View File

@@ -468,8 +468,7 @@
"connected-since": "Connected since", "connected-since": "Connected since",
"endpoint": "Endpoint", "endpoint": "Endpoint",
"button-download": "Download configuration", "button-download": "Download configuration",
"button-email": "Send configuration via E-Mail", "button-email": "Send configuration via E-Mail"
"style-label": "Configuration Style"
}, },
"peer-edit": { "peer-edit": {
"headline-edit-peer": "Edit peer:", "headline-edit-peer": "Edit peer:",

View File

@@ -142,8 +142,8 @@ export const peerStore = defineStore('peers', {
}) })
}) })
}, },
async MailPeerConfig(linkOnly, style, ids) { async MailPeerConfig(linkOnly, ids) {
return apiWrapper.post(`${baseUrl}/config-mail?style=${style}`, { return apiWrapper.post(`${baseUrl}/config-mail`, {
Identifiers: ids, Identifiers: ids,
LinkOnly: linkOnly LinkOnly: linkOnly
}) })
@@ -158,8 +158,8 @@ export const peerStore = defineStore('peers', {
throw new Error(error) throw new Error(error)
}) })
}, },
async LoadPeerConfig(id, style) { async LoadPeerConfig(id) {
return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}?style=${style}`) return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}`)
.then(this.setPeerConfig) .then(this.setPeerConfig)
.catch(error => { .catch(error => {
this.configuration = "" this.configuration = ""

View File

@@ -819,12 +819,6 @@
"schema": { "schema": {
"$ref": "#/definitions/model.PeerMailRequest" "$ref": "#/definitions/model.PeerMailRequest"
} }
},
{
"type": "string",
"description": "The configuration style",
"name": "style",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -864,12 +858,6 @@
"name": "id", "name": "id",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "string",
"description": "The configuration style",
"name": "style",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -911,12 +899,6 @@
"name": "id", "name": "id",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "string",
"description": "The configuration style",
"name": "style",
"in": "query"
} }
], ],
"responses": { "responses": {

View File

@@ -1072,10 +1072,6 @@ paths:
required: true required: true
schema: schema:
$ref: '#/definitions/model.PeerMailRequest' $ref: '#/definitions/model.PeerMailRequest'
- description: The configuration style
in: query
name: style
type: string
produces: produces:
- application/json - application/json
responses: responses:
@@ -1101,10 +1097,6 @@ paths:
name: id name: id
required: true required: true
type: string type: string
- description: The configuration style
in: query
name: style
type: string
produces: produces:
- image/png - image/png
- application/json - application/json
@@ -1133,10 +1125,6 @@ paths:
name: id name: id
required: true required: true
type: string type: string
- description: The configuration style
in: query
name: style
type: string
produces: produces:
- application/json - application/json
responses: responses:

View File

@@ -27,12 +27,12 @@ type PeerServicePeerManager interface {
} }
type PeerServiceConfigFileManager interface { type PeerServiceConfigFileManager interface {
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
} }
type PeerServiceMailManager interface { type PeerServiceMailManager interface {
SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error
} }
// endregion dependencies // endregion dependencies
@@ -95,24 +95,16 @@ func (p PeerService) DeletePeer(ctx context.Context, id domain.PeerIdentifier) e
return p.peers.DeletePeer(ctx, id) return p.peers.DeletePeer(ctx, id)
} }
func (p PeerService) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) { func (p PeerService) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
return p.configFile.GetPeerConfig(ctx, id, style) return p.configFile.GetPeerConfig(ctx, id)
} }
func (p PeerService) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) ( func (p PeerService) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
io.Reader, return p.configFile.GetPeerConfigQrCode(ctx, id)
error,
) {
return p.configFile.GetPeerConfigQrCode(ctx, id, style)
} }
func (p PeerService) SendPeerEmail( func (p PeerService) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error {
ctx context.Context, return p.mailer.SendPeerEmail(ctx, linkOnly, peers...)
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) { func (p PeerService) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error) {

View File

@@ -34,11 +34,11 @@ type PeerService interface {
// DeletePeer deletes the peer with the given id. // DeletePeer deletes the peer with the given id.
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
// GetPeerConfig returns the peer configuration for the given id. // GetPeerConfig returns the peer configuration for the given id.
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
// GetPeerConfigQrCode returns the peer configuration as qr code for the given id. // GetPeerConfigQrCode returns the peer configuration as qr code for the given id.
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
// SendPeerEmail sends the peer configuration via email. // SendPeerEmail sends the peer configuration via email.
SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error
// GetPeerStats returns the peer stats for the given interface. // GetPeerStats returns the peer stats for the given interface.
GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error)
} }
@@ -355,7 +355,6 @@ func (e PeerEndpoint) handleDelete() http.HandlerFunc {
// @Summary Get peer configuration as string. // @Summary Get peer configuration as string.
// @Produce json // @Produce json
// @Param id path string true "The peer identifier" // @Param id path string true "The peer identifier"
// @Param style query string false "The configuration style"
// @Success 200 {object} string // @Success 200 {object} string
// @Failure 400 {object} model.Error // @Failure 400 {object} model.Error
// @Failure 500 {object} model.Error // @Failure 500 {object} model.Error
@@ -370,9 +369,7 @@ func (e PeerEndpoint) handleConfigGet() http.HandlerFunc {
return return
} }
configStyle := e.getConfigStyle(r) configTxt, err := e.peerService.GetPeerConfig(r.Context(), domain.PeerIdentifier(id))
configTxt, err := e.peerService.GetPeerConfig(r.Context(), domain.PeerIdentifier(id), configStyle)
if err != nil { if err != nil {
respond.JSON(w, http.StatusInternalServerError, model.Error{ respond.JSON(w, http.StatusInternalServerError, model.Error{
Code: http.StatusInternalServerError, Message: err.Error(), Code: http.StatusInternalServerError, Message: err.Error(),
@@ -400,7 +397,6 @@ func (e PeerEndpoint) handleConfigGet() http.HandlerFunc {
// @Produce png // @Produce png
// @Produce json // @Produce json
// @Param id path string true "The peer identifier" // @Param id path string true "The peer identifier"
// @Param style query string false "The configuration style"
// @Success 200 {file} binary // @Success 200 {file} binary
// @Failure 400 {object} model.Error // @Failure 400 {object} model.Error
// @Failure 500 {object} model.Error // @Failure 500 {object} model.Error
@@ -415,9 +411,7 @@ func (e PeerEndpoint) handleQrCodeGet() http.HandlerFunc {
return return
} }
configStyle := e.getConfigStyle(r) configQr, err := e.peerService.GetPeerConfigQrCode(r.Context(), domain.PeerIdentifier(id))
configQr, err := e.peerService.GetPeerConfigQrCode(r.Context(), domain.PeerIdentifier(id), configStyle)
if err != nil { if err != nil {
respond.JSON(w, http.StatusInternalServerError, model.Error{ respond.JSON(w, http.StatusInternalServerError, model.Error{
Code: http.StatusInternalServerError, Message: err.Error(), Code: http.StatusInternalServerError, Message: err.Error(),
@@ -444,7 +438,6 @@ func (e PeerEndpoint) handleQrCodeGet() http.HandlerFunc {
// @Summary Send peer configuration via email. // @Summary Send peer configuration via email.
// @Produce json // @Produce json
// @Param request body model.PeerMailRequest true "The peer mail request data" // @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" // @Success 204 "No content if mail sending was successful"
// @Failure 400 {object} model.Error // @Failure 400 {object} model.Error
// @Failure 500 {object} model.Error // @Failure 500 {object} model.Error
@@ -467,13 +460,11 @@ func (e PeerEndpoint) handleEmailPost() http.HandlerFunc {
return return
} }
configStyle := e.getConfigStyle(r)
peerIds := make([]domain.PeerIdentifier, len(req.Identifiers)) peerIds := make([]domain.PeerIdentifier, len(req.Identifiers))
for i := range req.Identifiers { for i := range req.Identifiers {
peerIds[i] = domain.PeerIdentifier(req.Identifiers[i]) peerIds[i] = domain.PeerIdentifier(req.Identifiers[i])
} }
if err := e.peerService.SendPeerEmail(r.Context(), req.LinkOnly, configStyle, peerIds...); err != nil { if err := e.peerService.SendPeerEmail(r.Context(), req.LinkOnly, peerIds...); err != nil {
respond.JSON(w, http.StatusInternalServerError, respond.JSON(w, http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()}) model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return return
@@ -513,11 +504,3 @@ func (e PeerEndpoint) handleStatsGet() http.HandlerFunc {
respond.JSON(w, http.StatusOK, model.NewPeerStats(e.cfg.Statistics.CollectPeerData, stats)) 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
}

View File

@@ -23,8 +23,8 @@ type ProvisioningServicePeerManagerRepo interface {
} }
type ProvisioningServiceConfigFileManagerRepo interface { type ProvisioningServiceConfigFileManagerRepo interface {
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
} }
type ProvisioningService struct { type ProvisioningService struct {
@@ -96,7 +96,7 @@ func (p ProvisioningService) GetPeerConfig(ctx context.Context, peerId domain.Pe
return nil, err return nil, err
} }
peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier, domain.ConfigStyleWgQuick) peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -119,7 +119,7 @@ func (p ProvisioningService) GetPeerQrPng(ctx context.Context, peerId domain.Pee
return nil, err return nil, err
} }
peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier, domain.ConfigStyleWgQuick) peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -46,7 +46,7 @@ type TemplateRenderer interface {
// GetInterfaceConfig returns the configuration file for the given interface. // GetInterfaceConfig returns the configuration file for the given interface.
GetInterfaceConfig(iface *domain.Interface, peers []domain.Peer) (io.Reader, error) GetInterfaceConfig(iface *domain.Interface, peers []domain.Peer) (io.Reader, error)
// GetPeerConfig returns the configuration file for the given peer. // GetPeerConfig returns the configuration file for the given peer.
GetPeerConfig(peer *domain.Peer, style string) (io.Reader, error) GetPeerConfig(peer *domain.Peer) (io.Reader, error)
} }
type EventBus interface { 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. // GetPeerConfig returns the configuration file for the given peer.
// The file is structured in wg-quick format. // The file is structured in wg-quick format.
func (m Manager) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) { func (m Manager) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
peer, err := m.wg.GetPeer(ctx, id) peer, err := m.wg.GetPeer(ctx, id)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err) 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, st
return nil, err return nil, err
} }
return m.tplHandler.GetPeerConfig(peer, style) return m.tplHandler.GetPeerConfig(peer)
} }
// GetPeerConfigQrCode returns a QR code image containing the configuration for the given peer. // GetPeerConfigQrCode returns a QR code image containing the configuration for the given peer.
func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) { func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
peer, err := m.wg.GetPeer(ctx, id) peer, err := m.wg.GetPeer(ctx, id)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err) 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 return nil, err
} }
cfgData, err := m.tplHandler.GetPeerConfig(peer, style) cfgData, err := m.tplHandler.GetPeerConfig(peer)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get peer config for %s: %w", id, err) return nil, fmt.Errorf("failed to get peer config for %s: %w", id, err)
} }

View File

@@ -55,12 +55,11 @@ func (c TemplateHandler) GetInterfaceConfig(cfg *domain.Interface, peers []domai
} }
// GetPeerConfig returns the rendered configuration file for a WireGuard peer. // GetPeerConfig returns the rendered configuration file for a WireGuard peer.
func (c TemplateHandler) GetPeerConfig(peer *domain.Peer, style string) (io.Reader, error) { func (c TemplateHandler) GetPeerConfig(peer *domain.Peer) (io.Reader, error) {
var tplBuff bytes.Buffer var tplBuff bytes.Buffer
err := c.templates.ExecuteTemplate(&tplBuff, "wg_peer.tpl", map[string]any{ err := c.templates.ExecuteTemplate(&tplBuff, "wg_peer.tpl", map[string]any{
"Style": style, "Peer": peer,
"Peer": peer,
"Portal": map[string]any{ "Portal": map[string]any{
"Version": "unknown", "Version": "unknown",
}, },

View File

@@ -1,8 +1,6 @@
# AUTOGENERATED FILE - DO NOT EDIT # AUTOGENERATED FILE - DO NOT EDIT
# This file uses {{ .Style }} format. # This file uses wg-quick format.
{{- if eq .Style "wgquick"}}
# See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION # See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION
{{- end}}
# Lines starting with the -WGP- tag are used by # Lines starting with the -WGP- tag are used by
# the WireGuard Portal configuration parser. # the WireGuard Portal configuration parser.
@@ -23,27 +21,22 @@
# Core settings # Core settings
PrivateKey = {{ .Peer.Interface.KeyPair.PrivateKey }} PrivateKey = {{ .Peer.Interface.KeyPair.PrivateKey }}
{{- if eq .Style "wgquick"}}
Address = {{ CidrsToString .Peer.Interface.Addresses }} Address = {{ CidrsToString .Peer.Interface.Addresses }}
{{- end}}
# Misc. settings (optional) # Misc. settings (optional)
{{- if eq .Style "wgquick"}}
{{- if .Peer.Interface.DnsStr.GetValue}} {{- if .Peer.Interface.DnsStr.GetValue}}
DNS = {{ .Peer.Interface.DnsStr.GetValue }} {{- if .Peer.Interface.DnsSearchStr.GetValue}}, {{ .Peer.Interface.DnsSearchStr.GetValue }} {{- end}} DNS = {{ .Peer.Interface.DnsStr.GetValue }} {{- if .Peer.Interface.DnsSearchStr.GetValue}}, {{ .Peer.Interface.DnsSearchStr.GetValue }} {{- end}}
{{- end}} {{- end}}
{{- if ne .Peer.Interface.Mtu.GetValue 0}} {{- if ne .Peer.Interface.Mtu.GetValue 0}}
MTU = {{ .Peer.Interface.Mtu.GetValue }} MTU = {{ .Peer.Interface.Mtu.GetValue }}
{{- end}} {{- end}}
{{- if ne .Peer.Interface.RoutingTable.GetValue ""}}
Table = {{ .Peer.Interface.RoutingTable.GetValue }}
{{- end}}
{{- end}}
{{- if ne .Peer.Interface.FirewallMark.GetValue 0}} {{- if ne .Peer.Interface.FirewallMark.GetValue 0}}
FwMark = {{ .Peer.Interface.FirewallMark.GetValue }} FwMark = {{ .Peer.Interface.FirewallMark.GetValue }}
{{- end}} {{- end}}
{{- if ne .Peer.Interface.RoutingTable.GetValue ""}}
Table = {{ .Peer.Interface.RoutingTable.GetValue }}
{{- end}}
{{- if eq .Style "wgquick"}}
# Interface hooks (optional) # Interface hooks (optional)
{{- if .Peer.Interface.PreUp.GetValue}} {{- if .Peer.Interface.PreUp.GetValue}}
PreUp = {{ .Peer.Interface.PreUp.GetValue }} PreUp = {{ .Peer.Interface.PreUp.GetValue }}
@@ -57,7 +50,6 @@ PreDown = {{ .Peer.Interface.PreDown.GetValue }}
{{- if .Peer.Interface.PostDown.GetValue}} {{- if .Peer.Interface.PostDown.GetValue}}
PostDown = {{ .Peer.Interface.PostDown.GetValue }} PostDown = {{ .Peer.Interface.PostDown.GetValue }}
{{- end}} {{- end}}
{{- end}}
[Peer] [Peer]
PublicKey = {{ .Peer.EndpointPublicKey.GetValue }} PublicKey = {{ .Peer.EndpointPublicKey.GetValue }}

View File

@@ -21,9 +21,9 @@ type ConfigFileManager interface {
// GetInterfaceConfig returns the configuration for the given interface. // GetInterfaceConfig returns the configuration for the given interface.
GetInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) (io.Reader, error) GetInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) (io.Reader, error)
// GetPeerConfig returns the configuration for the given peer. // GetPeerConfig returns the configuration for the given peer.
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
// GetPeerConfigQrCode returns the QR code for the given peer. // GetPeerConfigQrCode returns the QR code for the given peer.
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
} }
type UserDatabaseRepo interface { type UserDatabaseRepo interface {
@@ -89,7 +89,7 @@ func NewMailManager(
} }
// SendPeerEmail sends an email to the user linked to the given peers. // SendPeerEmail sends an email to the user linked to the given peers.
func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error { func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error {
for _, peerId := range peers { for _, peerId := range peers {
peer, err := m.wg.GetPeer(ctx, peerId) peer, err := m.wg.GetPeer(ctx, peerId)
if err != nil { if err != nil {
@@ -123,7 +123,7 @@ func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, style string,
continue continue
} }
err = m.sendPeerEmail(ctx, linkOnly, style, user, peer) err = m.sendPeerEmail(ctx, linkOnly, user, peer)
if err != nil { if err != nil {
return fmt.Errorf("failed to send peer email for %s: %w", peerId, err) return fmt.Errorf("failed to send peer email for %s: %w", peerId, err)
} }
@@ -132,13 +132,7 @@ func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, style string,
return nil return nil
} }
func (m Manager) sendPeerEmail( func (m Manager) sendPeerEmail(ctx context.Context, linkOnly bool, user *domain.User, peer *domain.Peer) error {
ctx context.Context,
linkOnly bool,
style string,
user *domain.User,
peer *domain.Peer,
) error {
qrName := "WireGuardQRCode.png" qrName := "WireGuardQRCode.png"
configName := peer.GetConfigFileName() configName := peer.GetConfigFileName()
@@ -154,12 +148,12 @@ func (m Manager) sendPeerEmail(
} }
} else { } else {
peerConfig, err := m.configFiles.GetPeerConfig(ctx, peer.Identifier, style) peerConfig, err := m.configFiles.GetPeerConfig(ctx, peer.Identifier)
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch peer config for %s: %w", peer.Identifier, err) return fmt.Errorf("failed to fetch peer config for %s: %w", peer.Identifier, err)
} }
peerConfigQr, err := m.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier, style) peerConfigQr, err := m.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch peer config QR code for %s: %w", peer.Identifier, err) return fmt.Errorf("failed to fetch peer config QR code for %s: %w", peer.Identifier, err)
} }

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"github.com/h44z/wg-portal/internal/app" "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/config"
"github.com/h44z/wg-portal/internal/domain" "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) { func (m Manager) handleUserCreateEvent(user domain.User) {
m.handleGenericEvent(WebhookEventCreate, user) m.handleGenericEvent(WebhookEventCreate, models.NewUser(user))
} }
func (m Manager) handleUserUpdateEvent(user domain.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) { func (m Manager) handleUserDeleteEvent(user domain.User) {
m.handleGenericEvent(WebhookEventDelete, user) m.handleGenericEvent(WebhookEventDelete, models.NewUser(user))
} }
func (m Manager) handlePeerCreateEvent(peer domain.Peer) { func (m Manager) handlePeerCreateEvent(peer domain.Peer) {
m.handleGenericEvent(WebhookEventCreate, peer) m.handleGenericEvent(WebhookEventCreate, models.NewPeer(peer))
} }
func (m Manager) handlePeerUpdateEvent(peer domain.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) { func (m Manager) handlePeerDeleteEvent(peer domain.Peer) {
m.handleGenericEvent(WebhookEventDelete, peer) m.handleGenericEvent(WebhookEventDelete, models.NewPeer(peer))
} }
func (m Manager) handleInterfaceCreateEvent(iface domain.Interface) { func (m Manager) handleInterfaceCreateEvent(iface domain.Interface) {
m.handleGenericEvent(WebhookEventCreate, iface) m.handleGenericEvent(WebhookEventCreate, models.NewInterface(iface))
} }
func (m Manager) handleInterfaceUpdateEvent(iface domain.Interface) { func (m Manager) handleInterfaceUpdateEvent(iface domain.Interface) {
m.handleGenericEvent(WebhookEventUpdate, iface) m.handleGenericEvent(WebhookEventUpdate, models.NewInterface(iface))
} }
func (m Manager) handleInterfaceDeleteEvent(iface domain.Interface) { 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 { if peerStatus.IsConnected {
m.handleGenericEvent(WebhookEventConnect, peerStatus) m.handleGenericEvent(WebhookEventConnect, models.NewPeerMetrics(peerStatus, peer))
} else { } 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) { switch v := payload.(type) {
case domain.User: case models.User:
d.Entity = WebhookEntityUser d.Entity = WebhookEntityUser
d.Identifier = string(v.Identifier) d.Identifier = v.Identifier
case domain.Peer: case models.Peer:
d.Entity = WebhookEntityPeer d.Entity = WebhookEntityPeer
d.Identifier = string(v.Identifier) d.Identifier = v.Identifier
case domain.Interface: case models.Interface:
d.Entity = WebhookEntityInterface d.Entity = WebhookEntityInterface
d.Identifier = string(v.Identifier) d.Identifier = v.Identifier
case domain.PeerStatus: case models.PeerMetrics:
d.Entity = WebhookEntityPeer d.Entity = WebhookEntityPeerMetric
d.Identifier = string(v.PeerId) d.Identifier = v.Peer.Identifier
default: default:
return nil, fmt.Errorf("unsupported payload type: %T", v) return nil, fmt.Errorf("unsupported payload type: %T", v)
} }

View File

@@ -34,9 +34,10 @@ func (d *WebhookData) Serialize() (io.Reader, error) {
type WebhookEntity = string type WebhookEntity = string
const ( const (
WebhookEntityUser WebhookEntity = "user" WebhookEntityUser WebhookEntity = "user"
WebhookEntityPeer WebhookEntity = "peer" WebhookEntityPeer WebhookEntity = "peer"
WebhookEntityInterface WebhookEntity = "interface" WebhookEntityPeerMetric WebhookEntity = "peer_metric"
WebhookEntityInterface WebhookEntity = "interface"
) )
type WebhookEvent = string type WebhookEvent = string

View File

@@ -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,
}
}

View File

@@ -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(),
}
}

View File

@@ -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),
}
}

View File

@@ -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,
}
}

View File

@@ -213,8 +213,14 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
} }
if connectionStateChanged { 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 // 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 { if connectionStateChanged {
// publish event if connection state changed // publish event if connection state changed
c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus) c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus, peer)
} }
} }
} }

View File

@@ -62,7 +62,4 @@ const (
LockedReasonAdmin = "locked by admin" LockedReasonAdmin = "locked by admin"
LockedReasonApi = "locked by admin" LockedReasonApi = "locked by admin"
ConfigStyleRaw = "raw"
ConfigStyleWgQuick = "wgquick"
) )