Compare commits

..

2 Commits

Author SHA1 Message Date
Christoph
1d7dd26290 add webhook docs (#444) 2025-06-27 12:02:16 +02:00
Christoph Haas
3c72a26e91 add webhook event for peer state change (#444)
new event types: connect and disconnect

example payload:

```json
{
  "event": "connect",
  "entity": "peer",
  "identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
  "payload": {
    "PeerId": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
    "IsConnected": true,
    "IsPingable": false,
    "LastPing": null,
    "BytesReceived": 1860,
    "BytesTransmitted": 10824,
    "LastHandshake": "2025-06-26T23:04:33.325216659+02:00",
    "Endpoint": "10.55.66.77:33874",
    "LastSessionStart": "2025-06-26T22:50:40.10221606+02:00"
  }
}
```
2025-06-26 23:25:53 +02:00
50 changed files with 306 additions and 1076 deletions

View File

@@ -50,7 +50,7 @@ COPY --from=builder /build/dist/wg-portal /
######
# Final image
######
FROM alpine:3.22
FROM alpine:3.19
# Install OS-level dependencies
RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools
# Setup timezone

View File

@@ -87,7 +87,6 @@ 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)

View File

@@ -76,7 +76,6 @@ auth:
webauthn:
enabled: true
min_password_length: 16
hide_login_form: false
web:
listening_address: :8888
@@ -355,12 +354,6 @@ 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
@@ -673,6 +666,19 @@ 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`

View File

@@ -38,12 +38,11 @@ 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 models are supported for webhook events:
The following entity types can trigger webhooks:
- `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.
- `user`: When a WireGuard Portal user is created, updated, or deleted.
- `peer`: When a peer is created, updated, or deleted. This entity can also trigger `connect` and `disconnect` events.
- `interface`: When a device is created, updated, or deleted.
## Payload Structure
@@ -52,234 +51,36 @@ 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
"event": "create",
"entity": "user",
"identifier": "the-user-identifier",
"payload": {
// The payload of the event, e.g. a Peer model.
// Detailed model descriptions are provided below.
// The payload of the event, e.g. peer data.
// Check the API documentation for the exact structure.
}
}
```
### 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
### Example Payload
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
"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"
}
}
```

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<html lang="en">
<head>
<meta charset="UTF-8" />
<link href="/favicon.ico" rel="icon" />

View File

@@ -14,8 +14,8 @@
"@popperjs/core": "^2.11.8",
"@simplewebauthn/browser": "^13.1.0",
"@vojtechlanka/vue-tags-input": "^3.1.1",
"bootstrap": "^5.3.7",
"bootswatch": "^5.3.7",
"bootstrap": "^5.3.5",
"bootswatch": "^5.3.5",
"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.7",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz",
"integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==",
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz",
"integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==",
"funding": [
{
"type": "github",
@@ -1067,9 +1067,9 @@
}
},
"node_modules/bootswatch": {
"version": "5.3.7",
"resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.3.7.tgz",
"integrity": "sha512-n0X99+Jmpmd4vgkli5KwMOuAkgdyUPhq7cIAwoGXbM6WhE/mmkWACfxpr7WZeG9Pdx509Ndi+2K1HlzXXOr8/Q==",
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-5.3.5.tgz",
"integrity": "sha512-1z8LNoUL5NHmv/hNROALQ6qtjw9OJIjMgP8ovBlIft+oI15b/mvnzxGL896iO9LtoDZH0Vdm+D2YW+j03GduSg==",
"license": "MIT"
},
"node_modules/buffer-builder": {

View File

@@ -14,8 +14,8 @@
"@popperjs/core": "^2.11.8",
"@simplewebauthn/browser": "^13.1.0",
"@vojtechlanka/vue-tags-input": "^3.1.1",
"bootstrap": "^5.3.7",
"bootswatch": "^5.3.7",
"bootstrap": "^5.3.5",
"bootswatch": "^5.3.5",
"flag-icons": "^7.3.2",
"ip-address": "^10.0.1",
"is-cidr": "^5.1.1",

View File

@@ -14,10 +14,6 @@ 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();
@@ -44,13 +40,6 @@ 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();
@@ -136,24 +125,6 @@ const userDisplayName = computed(() => {
<div v-if="!auth.IsAuthenticated" class="nav-item">
<RouterLink :to="{ name: 'login' }" class="nav-link"><i class="fas fa-sign-in-alt fa-sm fa-fw me-2"></i>{{ $t('menu.login') }}</RouterLink>
</div>
<div class="nav-item dropdown" data-bs-theme="light">
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="theme-menu" aria-expanded="false" data-bs-toggle="dropdown" data-bs-display="static" aria-label="Toggle theme">
<i class="fa-solid fa-circle-half-stroke"></i>
<span class="d-lg-none ms-2">Toggle theme</span>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button type="button" class="dropdown-item d-flex align-items-center" @click.prevent="switchTheme('light')" aria-pressed="false">
<i class="fa-solid fa-sun"></i><span class="ms-2">Light</span>
</button>
</li>
<li>
<button type="button" class="dropdown-item d-flex align-items-center" @click.prevent="switchTheme('dark')" aria-pressed="true">
<i class="fa-solid fa-moon"></i><span class="ms-2">Dark</span>
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
@@ -170,7 +141,7 @@ const userDisplayName = computed(() => {
<div class="col-6 text-end">
<div :aria-label="$t('menu.lang')" class="btn-group" role="group">
<div class="btn-group" role="group">
<button aria-expanded="false" aria-haspopup="true" class="btn flag-button pe-0"
<button aria-expanded="false" aria-haspopup="true" class="btn btn btn-secondary pe-0"
data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button>
<div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style="">
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
@@ -192,31 +163,4 @@ const userDisplayName = computed(() => {
</footer>
</template>
<style>
.flag-button:active,.flag-button:hover,.flag-button:focus,.flag-button:checked,.flag-button:disabled,.flag-button:not(:disabled) {
border: 1px solid transparent!important;
}
[data-bs-theme=dark] .form-select {
color: #0c0c0c!important;
background-color: #c1c1c1!important;
--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")!important;
}
[data-bs-theme=dark] .form-control {
color: #0c0c0c!important;
background-color: #c1c1c1!important;
}
[data-bs-theme=dark] .form-control:focus {
color: #0c0c0c!important;
background-color: #c1c1c1!important;
}
[data-bs-theme=dark] .badge.bg-light {
--bs-bg-opacity: 1;
background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;
color: var(--bs-badge-color)!important;
}
[data-bs-theme=dark] span.input-group-text {
--bs-bg-opacity: 1;
background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;
color: var(--bs-badge-color)!important;
}
</style>
<style></style>

View File

@@ -65,14 +65,6 @@ a.disabled {
color: var(--bs-body-color);
}
[data-bs-theme=dark] .vue-tags-input .ti-tag {
position: relative;
background: #3c3c3c;
border: 2px solid var(--bs-body-color);
margin: 6px;
color: var(--bs-body-color);
}
/* the styles if a tag is invalid */
.vue-tags-input .ti-tag.ti-invalid {
background-color: #e88a74;

View File

@@ -50,7 +50,7 @@ const selectedStats = computed(() => {
if (!s) {
if (!!props.peerId || props.peerId.length) {
s = profile.Statistics(props.peerId)
p = profile.Statistics(props.peerId)
} else {
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) => {
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
}
})
watch(() => configStyle.value, async () => {
await peers.LoadPeerConfig(selectedPeer.value.Identifier, configStyle.value)
configString.value = peers.configuration
})
}
)
function download() {
// credit: https://www.bitdegree.org/learn/javascript-download
@@ -109,7 +103,7 @@ function download() {
}
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({
title: "Failed to send mail with peer configuration!",
text: e.toString(),
@@ -120,7 +114,7 @@ function email() {
function ConfigQrUrl() {
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 ''
}
@@ -130,15 +124,6 @@ function ConfigQrUrl() {
<template>
<Modal :title="title" :visible="visible" @close="close">
<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-dark 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-dark btn-sm" for="wgquick">WG-Quick</label>
</div>
</div>
<div class="accordion" id="peerInformation">
<div class="accordion-item">
<h2 class="accordion-header">
@@ -228,14 +213,6 @@ function ConfigQrUrl() {
</template>
</Modal></template>
<style>
.config-qr-img {
<style>.config-qr-img {
max-width: 100%;
}
.btn-switch-group .btn {
border-width: 1px;
padding: 5px;
line-height: 1;
}
</style>
}</style>

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ onMounted(async () => {
<div class="form-group d-inline">
<div class="input-group mb-3">
<input v-model="audit.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="audit.afterPageSizeChange">
<button class="btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
</div>
</div>
</div>

View File

@@ -13,21 +13,21 @@ const auth = authStore()
<p class="lead">{{ $t('home.abstract') }}</p>
<div class="card border-secondary p-5" v-if="auth.IsAuthenticated">
<div class="bg-light p-5" v-if="auth.IsAuthenticated">
<h2 class="display-5">{{ $t('home.profiles.headline') }}</h2>
<p class="lead">{{ $t('home.profiles.abstract') }}</p>
<hr class="my-4">
<p class="card-text">{{ $t('home.profiles.content') }}</p>
<p>{{ $t('home.profiles.content') }}</p>
<p class="lead">
<RouterLink :to="{ name: 'profile' }" class="btn btn-primary btn-lg">{{ $t('home.profiles.button') }}</RouterLink>
</p>
</div>
<div class="card border-secondary p-5 mt-4" v-if="auth.IsAuthenticated && auth.IsAdmin">
<div class="bg-light p-5 mt-4" v-if="auth.IsAuthenticated && auth.IsAdmin">
<h2 class="display-5">{{ $t('home.admin.headline') }}</h2>
<p class="lead">{{ $t('home.admin.abstract') }}</p>
<hr class="my-4">
<p class="card-text">{{ $t('home.admin.content') }}</p>
<p>{{ $t('home.admin.content') }}</p>
<p class="lead">
<RouterLink :to="{ name: 'interfaces' }" class="btn btn-primary btn-lg me-2">{{ $t('home.admin.button-admin') }}
</RouterLink>

View File

@@ -112,7 +112,7 @@ onMounted(async () => {
</div>
<div class="form-group">
<div class="input-group mb-3">
<button class="btn btn-primary" :title="$t('interfaces.button-add-interface')" @click.prevent="editInterfaceId='#NEW#'">
<button class="input-group-text btn btn-primary" :title="$t('interfaces.button-add-interface')" @click.prevent="editInterfaceId='#NEW#'">
<i class="fa-solid fa-plus-circle"></i>
</button>
<select v-model="interfaces.selected" :disabled="interfaces.Count===0" class="form-select" @change="() => { peers.LoadPeers(); peers.LoadStats() }">
@@ -314,7 +314,7 @@ onMounted(async () => {
<div class="form-group d-inline">
<div class="input-group mb-3">
<input v-model="peers.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="peers.afterPageSizeChange">
<button class="btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
</div>
</div>
</div>
@@ -429,5 +429,3 @@ onMounted(async () => {
</div>
</div>
</template>
<style>
</style>

View File

@@ -16,10 +16,7 @@ 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()
@@ -101,7 +98,7 @@ const externalLogin = function (provider) {
</div></div>
<div class="card-body">
<form method="post">
<fieldset v-if="showLoginForm">
<fieldset>
<div class="form-group">
<label class="form-label" for="inputUsername">{{ $t('login.username.label') }}</label>
<div class="input-group mb-3">
@@ -121,40 +118,19 @@ const externalLogin = function (provider) {
</div>
<div class="row mt-5 mb-2">
<div class="col-sm-4 col-xs-12">
<button :disabled="disableLoginBtn" class="btn btn-primary mb-2" type="submit" @click.prevent="login">
<div class="col-lg-4">
<button :disabled="disableLoginBtn" class="btn btn-primary" type="submit" @click.prevent="login">
{{ $t('login.button') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
</button>
</div>
<div class="col-sm-8 col-xs-12 text-sm-end">
<div class="col-lg-8 mb-2 text-end">
<button v-if="settings.Setting('WebAuthnEnabled')" class="btn btn-primary" type="submit" @click.prevent="loginWebAuthn">
{{ $t('login.button-webauthn') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
</button>
</div>
</div>
<div class="row mt-4 d-flex">
<div class="col-lg-12 d-flex mb-2">
<!-- OpenIdConnect / OAUTH providers -->
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
:disabled="loggingIn" :title="provider.Name" class="btn btn-outline-primary flex-fill"
v-html="provider.Name" @click.prevent="externalLogin(provider)"></button>
</div>
</div>
<div class="mt-3">
</div>
</fieldset>
<fieldset v-else>
<div class="row mt-1 mb-2" v-if="settings.Setting('WebAuthnEnabled')">
<div class="col-lg-12 d-flex mb-2">
<button class="btn btn-outline-primary flex-fill" type="submit" @click.prevent="loginWebAuthn">
{{ $t('login.button-webauthn') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
</button>
</div>
</div>
<div class="row mt-1 d-flex">
<div class="row mt-5 d-flex">
<div class="col-lg-12 d-flex mb-2">
<!-- OpenIdConnect / OAUTH providers -->
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
@@ -168,6 +144,7 @@ const externalLogin = function (provider) {
</fieldset>
</form>
</div>
</div>
</div>

View File

@@ -65,7 +65,7 @@ onMounted(async () => {
<div class="input-group mb-3">
<input v-model="profile.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text"
@keyup="profile.afterPageSizeChange">
<button class="btn btn-primary" :title="$t('general.search.button')"><i
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i
class="fa-solid fa-search"></i></button>
</div>
</div>
@@ -73,7 +73,7 @@ onMounted(async () => {
<div class="col-12 col-lg-3 text-lg-end">
<div class="form-group" v-if="settings.Setting('SelfProvisioning')">
<div class="input-group mb-3">
<button class="btn btn-primary" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'">
<button class="input-group-text btn btn-primary" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'">
<i class="fa fa-plus me-1"></i><i class="fa fa-user"></i>
</button>
<select v-model="profile.selectedInterfaceId" :disabled="profile.CountInterfaces===0" class="form-select">

View File

@@ -44,7 +44,7 @@ async function saveRename(credential) {
<p class="lead">{{ $t('settings.abstract') }}</p>
<div v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')">
<div class="card border-secondary p-5" v-if="profile.user.ApiToken">
<div class="bg-light p-5" v-if="profile.user.ApiToken">
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
<p class="lead">{{ $t('settings.api.abstract') }}</p>
<hr class="my-4">
@@ -72,7 +72,7 @@ async function saveRename(credential) {
</div>
<div class="row mt-5">
<div class="col-6">
<button class="btn btn-primary" :title="$t('settings.api.button-disable-title')" @click.prevent="profile.disableApi()" :disabled="profile.isFetching">
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-disable-title')" @click.prevent="profile.disableApi()" :disabled="profile.isFetching">
<i class="fa-solid fa-minus-circle"></i> {{ $t('settings.api.button-disable-text') }}
</button>
</div>
@@ -81,18 +81,18 @@ async function saveRename(credential) {
</div>
</div>
</div>
<div class="card border-secondary p-5" v-else>
<div class="bg-light p-5" v-else>
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
<p class="lead">{{ $t('settings.api.abstract') }}</p>
<hr class="my-4">
<p>{{ $t('settings.api.inactive-description') }}</p>
<button class="btn btn-primary" :title="$t('settings.api.button-enable-title')" @click.prevent="profile.enableApi()" :disabled="profile.isFetching">
<button class="input-group-text btn btn-primary" :title="$t('settings.api.button-enable-title')" @click.prevent="profile.enableApi()" :disabled="profile.isFetching">
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.api.button-enable-text') }}
</button>
</div>
</div>
<div class="card border-secondary p-5 mt-5" v-if="settings.Setting('WebAuthnEnabled')">
<div class="bg-light p-5 mt-5" v-if="settings.Setting('WebAuthnEnabled')">
<h2 class="display-7">{{ $t('settings.webauthn.headline') }}</h2>
<p class="lead">{{ $t('settings.webauthn.abstract') }}</p>
<hr class="my-4">
@@ -101,7 +101,7 @@ async function saveRename(credential) {
<div class="row">
<div class="col-6">
<button class="btn btn-primary" :title="$t('settings.webauthn.button-register-text')" @click.prevent="auth.RegisterWebAuthn" :disabled="auth.isFetching">
<button class="input-group-text btn btn-primary" :title="$t('settings.webauthn.button-register-text')" @click.prevent="auth.RegisterWebAuthn" :disabled="auth.isFetching">
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.webauthn.button-register-title') }}
</button>
</div>

View File

@@ -35,7 +35,7 @@ onMounted(() => {
<div class="form-group d-inline">
<div class="input-group mb-3">
<input v-model="users.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="users.afterPageSizeChange">
<button class="btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
</div>
</div>
</div>

31
go.mod
View File

@@ -4,31 +4,31 @@ go 1.24.0
require (
github.com/a8m/envsubst v1.4.3
github.com/alexedwards/scs/v2 v2.9.0
github.com/alexedwards/scs/v2 v2.8.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.27.0
github.com/go-webauthn/webauthn v0.13.4
github.com/go-playground/validator/v10 v10.26.0
github.com/go-webauthn/webauthn v0.13.0
github.com/google/uuid v1.6.0
github.com/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.5
github.com/swaggo/swag v1.16.4
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.40.0
golang.org/x/crypto v0.39.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sys v0.34.0
golang.org/x/sys v0.33.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlserver v1.6.1
gorm.io/driver/sqlserver v1.6.0
gorm.io/gorm v1.30.0
)
@@ -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.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.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,8 +53,8 @@ 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.23 // indirect
github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
github.com/go-webauthn/x v0.1.21 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/google/go-cmp v0.7.0 // 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.2 // indirect
github.com/microsoft/go-mssqldb v1.8.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
@@ -87,11 +87,10 @@ 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/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.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.33.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

152
go.sum
View File

@@ -1,14 +1,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.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.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.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
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.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.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=
@@ -17,7 +16,7 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occ
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/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 v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
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=
@@ -26,15 +25,14 @@ 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.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
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/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=
@@ -42,8 +40,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.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
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/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=
@@ -72,21 +70,22 @@ 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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
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.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.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.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/go-webauthn/webauthn v0.13.0 h1:cJIL1/1l+22UekVhipziAaSgESJxokYkowUqAIsWs0Y=
github.com/go-webauthn/webauthn v0.13.0/go.mod h1:Oy9o2o79dbLKRPZWWgRIOdtBGAhKnDIaBp2PFkICRHs=
github.com/go-webauthn/x v0.1.21 h1:nFbckQxudvHEJn2uy1VEi713MeSpApoAv9eRqsb9AdQ=
github.com/go-webauthn/x v0.1.21/go.mod h1:sEYohtg1zL4An1TXIUIQ5csdmoO+WO0R4R2pGKaHYKA=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-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-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -99,6 +98,7 @@ 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.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -119,10 +119,12 @@ github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFK
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
@@ -137,11 +139,8 @@ 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=
@@ -158,22 +157,23 @@ 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.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-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=
@@ -188,25 +188,17 @@ 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.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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.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.5 h1:nMf2fEV1TetMTJb4XzD0Lz7jFfKJmJKGTygEey8NSxM=
github.com/swaggo/swag v1.16.5/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
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,48 +220,40 @@ github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.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.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
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.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/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
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.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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
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/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
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=
@@ -279,68 +263,58 @@ 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/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.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/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.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=
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.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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.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.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/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-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.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/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
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=
@@ -351,19 +325,21 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
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=

View File

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

View File

@@ -381,8 +381,6 @@ definitions:
properties:
ApiAdminOnly:
type: boolean
LoginFormVisible:
type: boolean
MailLinkOnly:
type: boolean
MinPasswordLength:
@@ -1072,10 +1070,6 @@ paths:
required: true
schema:
$ref: '#/definitions/model.PeerMailRequest'
- description: The configuration style
in: query
name: style
type: string
produces:
- application/json
responses:
@@ -1101,10 +1095,6 @@ paths:
name: id
required: true
type: string
- description: The configuration style
in: query
name: style
type: string
produces:
- image/png
- application/json
@@ -1133,10 +1123,6 @@ paths:
name: id
required: true
type: string
- description: The configuration style
in: query
name: style
type: string
produces:
- application/json
responses:

View File

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

View File

@@ -96,13 +96,10 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sessionUser := domain.GetUserInfo(r.Context())
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,
LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin,
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
})
} else {
respond.JSON(w, http.StatusOK, model.Settings{
@@ -112,7 +109,6 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
ApiAdminOnly: e.cfg.Advanced.ApiAdminOnly,
WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled,
MinPasswordLength: e.cfg.Auth.MinPasswordLength,
LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin,
})
}
}

View File

@@ -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, 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(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(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(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.
// @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
@@ -370,9 +369,7 @@ func (e PeerEndpoint) handleConfigGet() http.HandlerFunc {
return
}
configStyle := e.getConfigStyle(r)
configTxt, err := e.peerService.GetPeerConfig(r.Context(), domain.PeerIdentifier(id), configStyle)
configTxt, err := e.peerService.GetPeerConfig(r.Context(), domain.PeerIdentifier(id))
if err != nil {
respond.JSON(w, http.StatusInternalServerError, model.Error{
Code: http.StatusInternalServerError, Message: err.Error(),
@@ -400,7 +397,6 @@ 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
@@ -415,9 +411,7 @@ func (e PeerEndpoint) handleQrCodeGet() http.HandlerFunc {
return
}
configStyle := e.getConfigStyle(r)
configQr, err := e.peerService.GetPeerConfigQrCode(r.Context(), domain.PeerIdentifier(id), configStyle)
configQr, err := e.peerService.GetPeerConfigQrCode(r.Context(), domain.PeerIdentifier(id))
if err != nil {
respond.JSON(w, http.StatusInternalServerError, model.Error{
Code: http.StatusInternalServerError, Message: err.Error(),
@@ -444,7 +438,6 @@ 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
@@ -467,13 +460,11 @@ 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, configStyle, peerIds...); err != nil {
if err := e.peerService.SendPeerEmail(r.Context(), req.LinkOnly, peerIds...); err != nil {
respond.JSON(w, http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
@@ -513,11 +504,3 @@ 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
}

View File

@@ -12,5 +12,4 @@ type Settings struct {
ApiAdminOnly bool `json:"ApiAdminOnly"`
WebAuthnEnabled bool `json:"WebAuthnEnabled"`
MinPasswordLength int `json:"MinPasswordLength"`
LoginFormVisible bool `json:"LoginFormVisible"`
}

View File

@@ -23,8 +23,8 @@ type ProvisioningServicePeerManagerRepo interface {
}
type ProvisioningServiceConfigFileManagerRepo interface {
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error)
GetPeerConfigQrCode(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) (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, domain.ConfigStyleWgQuick)
peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier)
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, domain.ConfigStyleWgQuick)
peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
if err != nil {
return nil, err
}

View File

@@ -93,8 +93,6 @@ 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
}
@@ -104,136 +102,82 @@ 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),
oauthAuthenticators: make(map[string]AuthenticatorOauth, len(cfg.OpenIDConnect)+len(cfg.OAuth)),
ldapAuthenticators: make(map[string]AuthenticatorLdap, len(cfg.Ldap)),
cfg: cfg,
bus: bus,
users: users,
callbackUrlPrefix: fmt.Sprintf("%s/api/v0", extUrl),
}
parsedExtUrl, err := url.Parse(a.callbackUrlPrefix)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
err := a.setupExternalAuthProviders(ctx)
if err != nil {
return nil, fmt.Errorf("failed to parse external URL: %w", err)
return nil, err
}
a.callbackUrl = parsedExtUrl
return a, nil
}
// 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
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)
}
ticker := time.NewTicker(30 * time.Second) // Ticker for delay between retries
defer ticker.Stop()
a.oauthAuthenticators = make(map[string]AuthenticatorOauth, len(a.cfg.OpenIDConnect)+len(a.cfg.OAuth))
a.ldapAuthenticators = make(map[string]AuthenticatorLdap, len(a.cfg.Ldap))
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]
for i := range a.cfg.OpenIDConnect { // OIDC
providerCfg := &a.cfg.OpenIDConnect[i]
providerId := strings.ToLower(providerCfg.ProviderName)
if _, exists := a.oauthAuthenticators[providerId]; exists {
// 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
return fmt.Errorf("auth provider with name %s is already registerd", providerId)
}
redirectUrl := *a.callbackUrl
redirectUrl := *extUrl
redirectUrl.Path = path.Join(redirectUrl.Path, "/auth/login/", providerId, "/callback")
provider, err := newOidcAuthenticator(ctx, redirectUrl.String(), providerCfg)
if err != nil {
failedOidc = append(failedOidc, oidc[i])
slog.Error("failed to setup oidc authentication provider", "name", providerId, "error", err)
continue
return fmt.Errorf("failed to setup oidc authentication provider %s: %w", providerCfg.ProviderName, err)
}
a.oauthAuthenticators[providerId] = provider
}
for i := range oauth { // PLAIN OAUTH
providerCfg := &oauth[i]
for i := range a.cfg.OAuth { // PLAIN OAUTH
providerCfg := &a.cfg.OAuth[i]
providerId := strings.ToLower(providerCfg.ProviderName)
if _, exists := a.oauthAuthenticators[providerId]; exists {
// 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
return fmt.Errorf("auth provider with name %s is already registerd", providerId)
}
redirectUrl := *a.callbackUrl
redirectUrl := *extUrl
redirectUrl.Path = path.Join(redirectUrl.Path, "/auth/login/", providerId, "/callback")
provider, err := newPlainOauthAuthenticator(ctx, redirectUrl.String(), providerCfg)
if err != nil {
failedOauth = append(failedOauth, oauth[i])
slog.Error("failed to setup oauth authentication provider", "name", providerId, "error", err)
continue
return fmt.Errorf("failed to setup oauth authentication provider %s: %w", providerId, err)
}
a.oauthAuthenticators[providerId] = provider
}
for i := range ldap { // LDAP
providerCfg := &ldap[i]
for i := range a.cfg.Ldap { // LDAP
providerCfg := &a.cfg.Ldap[i]
providerId := strings.ToLower(providerCfg.URL)
if _, exists := a.ldapAuthenticators[providerId]; exists {
// 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
return fmt.Errorf("auth provider with name %s is already registerd", providerId)
}
provider, err := newLdapAuthenticator(ctx, providerCfg)
if err != nil {
failedLdap = append(failedLdap, ldap[i])
slog.Error("failed to setup ldap authentication provider", "name", providerId, "error", err)
continue
return fmt.Errorf("failed to setup ldap authentication provider %s: %w", providerId, err)
}
a.ldapAuthenticators[providerId] = provider
}
return failedOidc, failedOauth, failedLdap
return nil
}
// GetExternalLoginProviders returns a list of all available external login providers.
@@ -490,10 +434,6 @@ 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(),
@@ -510,6 +450,10 @@ 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,

View File

@@ -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, style string) (io.Reader, error)
GetPeerConfig(peer *domain.Peer) (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, 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)
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, st
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.
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)
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, style)
cfgData, err := m.tplHandler.GetPeerConfig(peer)
if err != nil {
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.
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
err := c.templates.ExecuteTemplate(&tplBuff, "wg_peer.tpl", map[string]any{
"Style": style,
"Peer": peer,
"Peer": peer,
"Portal": map[string]any{
"Version": "unknown",
},

View File

@@ -1,8 +1,6 @@
# AUTOGENERATED FILE - DO NOT EDIT
# This file uses {{ .Style }} format.
{{- if eq .Style "wgquick"}}
# This file uses wg-quick format.
# 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.
@@ -23,27 +21,22 @@
# 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.RoutingTable.GetValue ""}}
Table = {{ .Peer.Interface.RoutingTable.GetValue }}
{{- end}}
{{- 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}}
{{- if eq .Style "wgquick"}}
# Interface hooks (optional)
{{- if .Peer.Interface.PreUp.GetValue}}
PreUp = {{ .Peer.Interface.PreUp.GetValue }}
@@ -57,7 +50,6 @@ PreDown = {{ .Peer.Interface.PreDown.GetValue }}
{{- if .Peer.Interface.PostDown.GetValue}}
PostDown = {{ .Peer.Interface.PostDown.GetValue }}
{{- end}}
{{- end}}
[Peer]
PublicKey = {{ .Peer.EndpointPublicKey.GetValue }}

View File

@@ -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, 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(ctx context.Context, id domain.PeerIdentifier, style string) (io.Reader, error)
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
}
type UserDatabaseRepo interface {
@@ -71,7 +71,7 @@ func NewMailManager(
users UserDatabaseRepo,
wg WireguardDatabaseRepo,
) (*Manager, error) {
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl, cfg.Web.SiteTitle)
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl)
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, style string, peers ...domain.PeerIdentifier) error {
func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, 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, style string,
continue
}
err = m.sendPeerEmail(ctx, linkOnly, style, user, peer)
err = m.sendPeerEmail(ctx, linkOnly, user, peer)
if err != nil {
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
}
func (m Manager) sendPeerEmail(
ctx context.Context,
linkOnly bool,
style string,
user *domain.User,
peer *domain.Peer,
) error {
func (m Manager) sendPeerEmail(ctx context.Context, linkOnly bool, user *domain.User, peer *domain.Peer) error {
qrName := "WireGuardQRCode.png"
configName := peer.GetConfigFileName()
@@ -154,12 +148,12 @@ func (m Manager) sendPeerEmail(
}
} else {
peerConfig, err := m.configFiles.GetPeerConfig(ctx, peer.Identifier, style)
peerConfig, err := m.configFiles.GetPeerConfig(ctx, peer.Identifier)
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, style)
peerConfigQr, err := m.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
if err != nil {
return fmt.Errorf("failed to fetch peer config QR code for %s: %w", peer.Identifier, err)
}

View File

@@ -17,12 +17,11 @@ 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, portalName string) (*TemplateHandler, error) {
func newTemplateHandler(portalUrl 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)
@@ -35,7 +34,6 @@ func newTemplateHandler(portalUrl, portalName string) (*TemplateHandler, error)
handler := &TemplateHandler{
portalUrl: portalUrl,
portalName: portalName,
htmlTemplates: htmlTemplateCache,
textTemplates: txtTemplateCache,
}
@@ -83,7 +81,6 @@ 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)
@@ -94,7 +91,6 @@ 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)

View File

@@ -19,7 +19,7 @@
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
<!--<![endif]-->
<title>{{$.PortalName}}</title>
<title>Email Template</title>
<!--[if gte mso 9]>
<style type="text/css" media="all">
sup { font-size: 100% !important; }
@@ -143,7 +143,7 @@
<td align="left">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td class="blue-button text-button" style="background:#000000; color:#ffffff; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
<td class="blue-button text-button" style="background:#000000; color:#c1cddc; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
</tr>
</table>
</td>
@@ -167,10 +167,10 @@
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#ffffff">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated by {{$.PortalName}}.</td>
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated using WireGuard Portal.</td>
</tr>
<tr>
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit {{$.PortalName}}</span></a></td>
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit WireGuard Portal</span></a></td>
</tr>
</table>
</td>

View File

@@ -20,5 +20,5 @@ You can download and install the WireGuard VPN client from:
https://www.wireguard.com/install/
This mail was generated by {{$.PortalName}}.
This mail was generated using WireGuard Portal.
{{$.PortalUrl}}

View File

@@ -19,7 +19,7 @@
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
<!--<![endif]-->
<title>{{$.PortalName}}</title>
<title>Email Template</title>
<!--[if gte mso 9]>
<style type="text/css" media="all">
sup { font-size: 100% !important; }
@@ -143,7 +143,7 @@
<td align="left">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td class="blue-button text-button" style="background:#000000; color:#ffffff; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
<td class="blue-button text-button" style="background:#000000; color:#c1cddc; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
</tr>
</table>
</td>
@@ -167,10 +167,10 @@
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#ffffff">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated by {{$.PortalName}}.</td>
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated using WireGuard Portal.</td>
</tr>
<tr>
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit {{$.PortalName}}</span></a></td>
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit WireGuard Portal</span></a></td>
</tr>
</table>
</td>

View File

@@ -20,5 +20,5 @@ You can download and install the WireGuard VPN client from:
https://www.wireguard.com/install/
This mail was generated by {{$.PortalName}}.
This mail was generated using WireGuard Portal.
{{$.PortalUrl}}

View File

@@ -8,7 +8,6 @@ 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"
)
@@ -102,46 +101,46 @@ func (m Manager) sendWebhook(ctx context.Context, data io.Reader) error {
}
func (m Manager) handleUserCreateEvent(user domain.User) {
m.handleGenericEvent(WebhookEventCreate, models.NewUser(user))
m.handleGenericEvent(WebhookEventCreate, user)
}
func (m Manager) handleUserUpdateEvent(user domain.User) {
m.handleGenericEvent(WebhookEventUpdate, models.NewUser(user))
m.handleGenericEvent(WebhookEventUpdate, user)
}
func (m Manager) handleUserDeleteEvent(user domain.User) {
m.handleGenericEvent(WebhookEventDelete, models.NewUser(user))
m.handleGenericEvent(WebhookEventDelete, user)
}
func (m Manager) handlePeerCreateEvent(peer domain.Peer) {
m.handleGenericEvent(WebhookEventCreate, models.NewPeer(peer))
m.handleGenericEvent(WebhookEventCreate, peer)
}
func (m Manager) handlePeerUpdateEvent(peer domain.Peer) {
m.handleGenericEvent(WebhookEventUpdate, models.NewPeer(peer))
m.handleGenericEvent(WebhookEventUpdate, peer)
}
func (m Manager) handlePeerDeleteEvent(peer domain.Peer) {
m.handleGenericEvent(WebhookEventDelete, models.NewPeer(peer))
m.handleGenericEvent(WebhookEventDelete, peer)
}
func (m Manager) handleInterfaceCreateEvent(iface domain.Interface) {
m.handleGenericEvent(WebhookEventCreate, models.NewInterface(iface))
m.handleGenericEvent(WebhookEventCreate, iface)
}
func (m Manager) handleInterfaceUpdateEvent(iface domain.Interface) {
m.handleGenericEvent(WebhookEventUpdate, models.NewInterface(iface))
m.handleGenericEvent(WebhookEventUpdate, iface)
}
func (m Manager) handleInterfaceDeleteEvent(iface domain.Interface) {
m.handleGenericEvent(WebhookEventDelete, models.NewInterface(iface))
m.handleGenericEvent(WebhookEventDelete, iface)
}
func (m Manager) handlePeerStateChangeEvent(peerStatus domain.PeerStatus, peer domain.Peer) {
func (m Manager) handlePeerStateChangeEvent(peerStatus domain.PeerStatus) {
if peerStatus.IsConnected {
m.handleGenericEvent(WebhookEventConnect, models.NewPeerMetrics(peerStatus, peer))
m.handleGenericEvent(WebhookEventConnect, peerStatus)
} else {
m.handleGenericEvent(WebhookEventDisconnect, models.NewPeerMetrics(peerStatus, peer))
m.handleGenericEvent(WebhookEventDisconnect, peerStatus)
}
}
@@ -178,18 +177,18 @@ func (m Manager) createWebhookData(action WebhookEvent, payload any) (*WebhookDa
}
switch v := payload.(type) {
case models.User:
case domain.User:
d.Entity = WebhookEntityUser
d.Identifier = v.Identifier
case models.Peer:
d.Identifier = string(v.Identifier)
case domain.Peer:
d.Entity = WebhookEntityPeer
d.Identifier = v.Identifier
case models.Interface:
d.Identifier = string(v.Identifier)
case domain.Interface:
d.Entity = WebhookEntityInterface
d.Identifier = v.Identifier
case models.PeerMetrics:
d.Entity = WebhookEntityPeerMetric
d.Identifier = v.Peer.Identifier
d.Identifier = string(v.Identifier)
case domain.PeerStatus:
d.Entity = WebhookEntityPeer
d.Identifier = string(v.PeerId)
default:
return nil, fmt.Errorf("unsupported payload type: %T", v)
}

View File

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

View File

@@ -1,99 +0,0 @@
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

@@ -1,89 +0,0 @@
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

@@ -1,50 +0,0 @@
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

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

View File

@@ -461,7 +461,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
physicalInterface, _ := m.wg.GetInterface(ctx, id)
if err := m.handleInterfacePreSaveHooks(existingInterface, !existingInterface.IsDisabled(), false); err != nil {
if err := m.handleInterfacePreSaveHooks(true, existingInterface); err != nil {
return fmt.Errorf("pre-delete hooks failed: %w", err)
}
@@ -490,7 +490,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
Table: existingInterface.GetRoutingTable(),
})
if err := m.handleInterfacePostSaveHooks(existingInterface, !existingInterface.IsDisabled(), false); err != nil {
if err := m.handleInterfacePostSaveHooks(true, existingInterface); err != nil {
return fmt.Errorf("post-delete hooks failed: %w", err)
}
@@ -509,9 +509,9 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
return nil, fmt.Errorf("interface validation failed: %w", err)
}
oldEnabled, newEnabled := m.getInterfaceStateHistory(ctx, iface)
stateChanged := m.hasInterfaceStateChanged(ctx, iface)
if err := m.handleInterfacePreSaveHooks(iface, oldEnabled, newEnabled); err != nil {
if err := m.handleInterfacePreSaveHooks(stateChanged, iface); err != nil {
return nil, fmt.Errorf("pre-save hooks failed: %w", err)
}
@@ -551,7 +551,7 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
m.bus.Publish(app.TopicRouteUpdate, "interface updated: "+string(iface.Identifier))
}
if err := m.handleInterfacePostSaveHooks(iface, oldEnabled, newEnabled); err != nil {
if err := m.handleInterfacePostSaveHooks(stateChanged, iface); err != nil {
return nil, fmt.Errorf("post-save hooks failed: %w", err)
}
@@ -566,13 +566,32 @@ func (m Manager) saveInterface(ctx context.Context, iface *domain.Interface) (
return iface, nil
}
func (m Manager) getInterfaceStateHistory(ctx context.Context, iface *domain.Interface) (oldEnabled, newEnabled bool) {
func (m Manager) hasInterfaceStateChanged(ctx context.Context, iface *domain.Interface) bool {
oldInterface, err := m.db.GetInterface(ctx, iface.Identifier)
if err != nil {
return false, !iface.IsDisabled() // if the interface did not exist, we assume it was not enabled
return false
}
return !oldInterface.IsDisabled(), !iface.IsDisabled()
if oldInterface.IsDisabled() != iface.IsDisabled() {
return true // interface in db has changed
}
wgInterface, err := m.wg.GetInterface(ctx, iface.Identifier)
if err != nil {
return true // interface might not exist - so we assume that there must be a change
}
// compare physical interface settings
if len(wgInterface.Addresses) != len(iface.Addresses) ||
wgInterface.Mtu != iface.Mtu ||
wgInterface.FirewallMark != iface.FirewallMark ||
wgInterface.ListenPort != iface.ListenPort ||
wgInterface.PrivateKey != iface.PrivateKey ||
wgInterface.PublicKey != iface.PublicKey {
return true
}
return false
}
func (m Manager) handleInterfacePreSaveActions(iface *domain.Interface) error {
@@ -588,14 +607,12 @@ func (m Manager) handleInterfacePreSaveActions(iface *domain.Interface) error {
return nil
}
func (m Manager) handleInterfacePreSaveHooks(iface *domain.Interface, oldEnabled, newEnabled bool) error {
if oldEnabled == newEnabled {
func (m Manager) handleInterfacePreSaveHooks(stateChanged bool, iface *domain.Interface) error {
if !stateChanged {
return nil // do nothing if state did not change
}
slog.Debug("executing pre-save hooks", "interface", iface.Identifier, "up", newEnabled)
if newEnabled {
if !iface.IsDisabled() {
if err := m.quick.ExecuteInterfaceHook(iface.Identifier, iface.PreUp); err != nil {
return fmt.Errorf("failed to execute pre-up hook: %w", err)
}
@@ -607,14 +624,12 @@ func (m Manager) handleInterfacePreSaveHooks(iface *domain.Interface, oldEnabled
return nil
}
func (m Manager) handleInterfacePostSaveHooks(iface *domain.Interface, oldEnabled, newEnabled bool) error {
if oldEnabled == newEnabled {
func (m Manager) handleInterfacePostSaveHooks(stateChanged bool, iface *domain.Interface) error {
if !stateChanged {
return nil // do nothing if state did not change
}
slog.Debug("executing post-save hooks", "interface", iface.Identifier, "up", newEnabled)
if newEnabled {
if !iface.IsDisabled() {
if err := m.quick.ExecuteInterfaceHook(iface.Identifier, iface.PostUp); err != nil {
return fmt.Errorf("failed to execute post-up hook: %w", err)
}

View File

@@ -21,9 +21,6 @@ type Auth struct {
// MinPasswordLength is the minimum password length for user accounts. This also applies to the admin user.
// It is encouraged to set this value to at least 16 characters.
MinPasswordLength int `yaml:"min_password_length"`
// HideLoginForm specifies whether the login form should be hidden. If no social login providers are configured,
// the login form will be shown regardless of this setting.
HideLoginForm bool `yaml:"hide_login_form"`
}
// BaseFields contains the basic fields that are used to map user information from the authentication providers.

View File

@@ -95,9 +95,6 @@ func (c *Config) LogStartupValues() {
"oidcProviders", len(c.Auth.OpenIDConnect),
"oauthProviders", len(c.Auth.OAuth),
"ldapProviders", len(c.Auth.Ldap),
"webauthnEnabled", c.Auth.WebAuthn.Enabled,
"minPasswordLength", c.Auth.MinPasswordLength,
"hideLoginForm", c.Auth.HideLoginForm,
)
}
@@ -172,7 +169,6 @@ func defaultConfig() *Config {
cfg.Auth.WebAuthn.Enabled = true
cfg.Auth.MinPasswordLength = 16
cfg.Auth.HideLoginForm = false
return cfg
}

View File

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