peer config and qr code

This commit is contained in:
Christoph Haas 2023-06-24 01:35:25 +02:00
parent 4a53a5207a
commit 9fdb8d8633
29 changed files with 379 additions and 185 deletions

View File

@ -36,7 +36,7 @@ const title = computed(() => {
watch(() => props.visible, async (newValue, oldValue) => {
if (oldValue === false && newValue === true) { // if modal is shown
console.log(selectedInterface.value)
await interfaces.InterfaceConfig(selectedInterface.value.Identifier)
await interfaces.LoadInterfaceConfig(selectedInterface.value.Identifier)
configString.value = interfaces.configuration
}
}

View File

@ -9,7 +9,7 @@ import Vue3TagsInput from "vue3-tags-input";
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
import isCidr from "is-cidr";
import {isIP} from 'is-ip';
import { freshPeer } from '@/helpers/models';
import { freshPeer, freshInterface } from '@/helpers/models';
const { t } = useI18n()
@ -31,10 +31,7 @@ const selectedInterface = computed(() => {
let i = interfaces.GetSelected;
if (!i) {
i = { // dummy interface to avoid 'undefined' exceptions
Identifier: "none",
Mode: "server"
}
i = freshInterface() // dummy interface to avoid 'undefined' exceptions
}
return i

View File

@ -2,7 +2,12 @@
import Modal from "./Modal.vue";
import {peerStore} from "@/stores/peers";
import {interfaceStore} from "@/stores/interfaces";
import {computed} from "vue";
import {computed, ref, watch} from "vue";
import {useI18n} from "vue-i18n";
import { freshInterface, freshPeer } from '@/helpers/models';
import Prism from "vue-prism-component";
const { t } = useI18n()
const peers = peerStore()
const interfaces = interfaceStore()
@ -18,30 +23,49 @@ function close() {
emit('close')
}
const configString = ref("")
const selectedPeer = computed(() => {
return peers.Find(props.peerId)
let p = peers.Find(props.peerId)
if (!p) {
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
}
return p
})
const selectedInterface = computed(() => {
let i = interfaces.GetSelected;
if (!i) {
i = { // dummy interface to avoid 'undefined' exceptions
Identifier: "none",
Mode: "server"
}
i = freshInterface() // dummy interface to avoid 'undefined' exceptions
}
return i
})
const title = computed(() => {
if (selectedPeer.value) {
return "Peer: " + selectedPeer.value.Name
if (!props.visible) {
return "" // otherwise interfaces.GetSelected will die...
}
if (selectedInterface.value.Mode === "server") {
return t("interfaces.peer.view") + ": " + selectedPeer.value.DisplayName
} else {
return t("interfaces.endpoint.view") + ": " + selectedPeer.value.DisplayName
}
return ""
})
watch(() => props.visible, async (newValue, oldValue) => {
if (oldValue === false && newValue === true) { // if modal is shown
console.log(selectedInterface.value)
console.log(selectedPeer.value)
await peers.LoadPeerConfig(selectedPeer.value.Identifier)
configString.value = peers.configuration
}
}
)
</script>
<template>
@ -60,16 +84,16 @@ const title = computed(() => {
<div class="col-md-8">
<h4>Details</h4>
<ul>
<li>Firstname: Some</li>
<li>Lastname: Username</li>
<li>Phone: 123456789</li>
<li>Mail: x@y.de</li>
<li>Identifier: {{ selectedPeer.PublicKey }}</li>
<li>IP Addresses: <span v-for="ip in selectedPeer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span></li>
<li>Linked User: {{ selectedPeer.UserIdentifier }}</li>
<li>Notes: {{ selectedPeer.Notes }}</li>
</ul>
<h4>Traffic</h4>
<p><i class="fas fa-long-arrow-alt-down"></i> 1.5 MB / <i class="fas fa-long-arrow-alt-up"></i> 3.9 MB</p>
</div>
<div class="col-md-4">
<img class="config-qr-img" src="https://hexdocs.pm/qr_code/docs/qrcode.svg">
<img class="config-qr-img" :src="peers.ConfigQrUrl(props.peerId)" loading="lazy" alt="Configuration QR Code">
</div>
</div>
</div>
@ -83,28 +107,7 @@ const title = computed(() => {
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#accordionExample" style="">
<div class="accordion-body">
<pre>
# AUTOGENERATED FILE - PROVIDED BY WIREGUARD PORTAL
# WireGuard configuration: Some username (Home)
# -WGP- PublicKey: xyz123
[Interface]
# Core settings
PrivateKey = abcd2131234
Address = 10.6.6.3/32, fd9f:6666::3/128
# Misc. settings (optional)
DNS = 10.10.1.20, fd9f:6666::10:6:6:1
MTU = 1380
[Peer]
PublicKey = oidjsfgsp9oih23
Endpoint = vpn.server.de:51820
AllowedIPs = 10.6.6.0/24, 10.10.0.0/16, 10.12.0.0/16, fd9f:6666::/64
PresharedKey = +1FPHPdsfjkln23
PersistentKeepalive = 16
</pre>
<Prism language="ini" :code="configString"></Prism>
</div>
</div>
</div>

View File

@ -0,0 +1,7 @@
export function base64_url_encode(input) {
let output = btoa(input)
output = output.replace('+', '.')
output = output.replace('/', '_')
output = output.replace('=', '-')
return output
}

View File

@ -2,6 +2,7 @@ import { authStore } from '../stores/auth';
import { securityStore } from '../stores/security';
export const fetchWrapper = {
url: apiUrl(),
get: request('GET'),
post: request('POST'),
put: request('PUT'),
@ -9,6 +10,7 @@ export const fetchWrapper = {
};
export const apiWrapper = {
url: apiUrl(),
get: apiRequest('GET'),
post: apiRequest('POST'),
put: apiRequest('PUT'),
@ -46,6 +48,13 @@ function apiRequest(method) {
}
}
// apiUrl uses WGPORTAL_BACKEND_BASE_URL as base URL
function apiUrl() {
return (path) => {
return WGPORTAL_BACKEND_BASE_URL + path
}
}
// helper functions
function getHeaders(method, url) {

View File

@ -54,9 +54,8 @@
"h2-client": "Aktuelle Endpunkte",
"tableHeadings": [
"Name",
"Kennung",
"Benutzer",
"IPs",
"IP's",
"Endpunkt",
"Handschlag"
],

View File

@ -51,9 +51,8 @@
"h2-client": "Current Endpoints",
"tableHeadings": [
"Name",
"Identifier",
"User",
"IPs",
"IP's",
"Endpoint",
"Handshake"
],

View File

@ -3,6 +3,7 @@ import { defineStore } from 'pinia'
import {apiWrapper} from '@/helpers/fetch-wrapper'
import {notify} from "@kyvg/vue3-notification";
import { freshInterface } from '@/helpers/models';
import { base64_url_encode } from '@/helpers/encoding';
const baseUrl = `/interface`
@ -66,21 +67,21 @@ export const interfaceStore = defineStore({
})
})
},
async InterfaceConfig(id) {
return apiWrapper.get(`${baseUrl}/config/${encodeURIComponent(id)}`)
.then(this.setInterfaceConfig)
.catch(error => {
this.prepared = {}
console.log("Failed to load interface configuration: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load interface configuration!",
})
async LoadInterfaceConfig(id) {
return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}`)
.then(this.setInterfaceConfig)
.catch(error => {
this.configuration = ""
console.log("Failed to load interface configuration: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load interface configuration!",
})
})
},
async DeleteInterface(id) {
this.fetching = true
return apiWrapper.delete(`${baseUrl}/${encodeURIComponent(id)}`)
return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
.then(() => {
this.interfaces = this.interfaces.filter(i => i.Identifier !== id)
if (this.interfaces.length > 0) {
@ -98,7 +99,7 @@ export const interfaceStore = defineStore({
},
async UpdateInterface(id, formData) {
this.fetching = true
return apiWrapper.put(`${baseUrl}/${encodeURIComponent(id)}`, formData)
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
.then(iface => {
let idx = this.interfaces.findIndex((i) => i.Identifier === id)
this.interfaces[idx] = iface

View File

@ -3,6 +3,7 @@ import {apiWrapper} from "../helpers/fetch-wrapper";
import {notify} from "@kyvg/vue3-notification";
import {interfaceStore} from "./interfaces";
import { freshPeer } from '@/helpers/models';
import { base64_url_encode } from '@/helpers/encoding';
const baseUrl = `/peer`
@ -12,6 +13,7 @@ export const peerStore = defineStore({
peers: [],
peer: freshPeer(),
prepared: freshPeer(),
configuration: "",
filter: "",
pageSize: 10,
pageOffset: 0,
@ -37,6 +39,9 @@ export const peerStore = defineStore({
FilteredAndPaged: (state) => {
return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
},
ConfigQrUrl: (state) => {
return (id) => apiWrapper.url(`${baseUrl}/config-qr/${base64_url_encode(id)}`)
},
isFetching: (state) => state.fetching,
hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
hasPrevPage: (state) => state.pageOffset > 0,
@ -82,8 +87,11 @@ export const peerStore = defineStore({
setPreparedPeer(peer) {
this.prepared = peer;
},
setPeerConfig(config) {
this.configuration = config;
},
async PreparePeer(interfaceId) {
return apiWrapper.get(`${baseUrl}/iface/${encodeURIComponent(interfaceId)}/prepare`)
return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/prepare`)
.then(this.setPreparedPeer)
.catch(error => {
this.prepared = freshPeer()
@ -94,9 +102,21 @@ export const peerStore = defineStore({
})
})
},
async LoadPeerConfig(id) {
return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}`)
.then(this.setPeerConfig)
.catch(error => {
this.configuration = ""
console.log("Failed to load peer configuration: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load peer configuration!",
})
})
},
async LoadPeer(id) {
this.fetching = true
return apiWrapper.get(`${baseUrl}/${encodeURIComponent(id)}`)
return apiWrapper.get(`${baseUrl}/${base64_url_encode(id)}`)
.then(this.setPeer)
.catch(error => {
this.setPeers([])
@ -109,7 +129,7 @@ export const peerStore = defineStore({
},
async DeletePeer(id) {
this.fetching = true
return apiWrapper.delete(`${baseUrl}/${encodeURIComponent(id)}`)
return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
.then(() => {
this.peers = this.peers.filter(p => p.Identifier !== id)
this.fetching = false
@ -122,7 +142,7 @@ export const peerStore = defineStore({
},
async UpdatePeer(id, formData) {
this.fetching = true
return apiWrapper.put(`${baseUrl}/${encodeURIComponent(id)}`, formData)
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
.then(peer => {
let idx = this.peers.findIndex((p) => p.Identifier === id)
this.peers[idx] = peer
@ -136,7 +156,7 @@ export const peerStore = defineStore({
},
async CreatePeer(interfaceId, formData) {
this.fetching = true
return apiWrapper.post(`${baseUrl}/iface/${encodeURIComponent(interfaceId)}/new`, formData)
return apiWrapper.post(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/new`, formData)
.then(peer => {
this.peers.push(peer)
this.fetching = false
@ -157,7 +177,7 @@ export const peerStore = defineStore({
}
this.fetching = true
return apiWrapper.get(`${baseUrl}/iface/${encodeURIComponent(interfaceId)}/all`)
return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/all`)
.then(this.setPeers)
.catch(error => {
this.setPeers([])

View File

@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import {apiWrapper} from "@/helpers/fetch-wrapper";
import {notify} from "@kyvg/vue3-notification";
import {authStore} from "@/stores/auth";
import { base64_url_encode } from '@/helpers/encoding';
const baseUrl = `/user`
@ -79,7 +80,7 @@ export const profileStore = defineStore({
async LoadPeers() {
this.fetching = true
let currentUser = authStore().user.Identifier
return apiWrapper.get(`${baseUrl}/${encodeURIComponent(currentUser)}/peers`)
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}/peers`)
.then(this.setPeers)
.catch(error => {
this.setPeers([])
@ -93,7 +94,7 @@ export const profileStore = defineStore({
async LoadUser() {
this.fetching = true
let currentUser = authStore().user.Identifier
return apiWrapper.get(`${baseUrl}/${encodeURIComponent(currentUser)}`)
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}`)
.then(this.setUser)
.catch(error => {
this.setUser({})

View File

@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import {apiWrapper} from "@/helpers/fetch-wrapper";
import {notify} from "@kyvg/vue3-notification";
import { base64_url_encode } from '@/helpers/encoding';
const baseUrl = `/user`
@ -91,7 +92,7 @@ export const userStore = defineStore({
},
async DeleteUser(id) {
this.fetching = true
return apiWrapper.delete(`${baseUrl}/${encodeURIComponent(id)}`)
return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
.then(() => {
this.users = this.users.filter(u => u.Identifier !== id)
this.fetching = false
@ -104,7 +105,7 @@ export const userStore = defineStore({
},
async UpdateUser(id, formData) {
this.fetching = true
return apiWrapper.put(`${baseUrl}/${encodeURIComponent(id)}`, formData)
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
.then(user => {
let idx = this.users.findIndex((u) => u.Identifier === id)
this.users[idx] = user
@ -131,7 +132,7 @@ export const userStore = defineStore({
},
async LoadUserPeers(id) {
this.fetching = true
return apiWrapper.get(`${baseUrl}/${encodeURIComponent(id)}/peers`)
return apiWrapper.get(`${baseUrl}/${base64_url_encode(id)}/peers`)
.then(this.setUserPeers)
.catch(error => {
this.setUserPeers([])

View File

@ -272,9 +272,8 @@ onMounted(async () => {
<th scope="col">{{ $t('interfaces.tableHeadings[0]') }}</th>
<th scope="col">{{ $t('interfaces.tableHeadings[1]') }}</th>
<th scope="col">{{ $t('interfaces.tableHeadings[2]') }}</th>
<th scope="col">{{ $t('interfaces.tableHeadings[3]') }}</th>
<th v-if="interfaces.GetSelected.Mode==='client'" scope="col">{{ $t('interfaces.tableHeadings[4]') }}</th>
<th scope="col">{{ $t('interfaces.tableHeadings[5]') }}</th>
<th v-if="interfaces.GetSelected.Mode==='client'" scope="col">{{ $t('interfaces.tableHeadings[3]') }}</th>
<th scope="col">{{ $t('interfaces.tableHeadings[4]') }}</th>
<th scope="col"></th><!-- Actions -->
</tr>
</thead>
@ -284,10 +283,9 @@ onMounted(async () => {
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
</th>
<td>{{peer.DisplayName}}</td>
<td>{{peer.Identifier}}</td>
<td>{{peer.UserIdentifier}}</td>
<td>
<span v-for="ip in peer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span>
<span v-for="ip in peer.Addresses" :key="ip" class="badge bg-light me-1">{{ ip }}</span>
</td>
<td v-if="interfaces.GetSelected.Mode==='client'">{{peer.Endpoint.Value}}</td>
<td>{{peer.LastConnected}}</td>

7
go.mod
View File

@ -19,6 +19,8 @@ require (
github.com/vardius/message-bus v1.1.5
github.com/vishvananda/netlink v1.1.0
github.com/xhit/go-simple-mail/v2 v2.13.0
github.com/yeqown/go-qrcode/v2 v2.2.1
github.com/yeqown/go-qrcode/writer/standard v1.2.1
golang.org/x/crypto v0.10.0
golang.org/x/oauth2 v0.9.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
@ -37,6 +39,7 @@ require (
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dchest/uniuri v1.2.0 // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
@ -53,6 +56,7 @@ require (
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.3.0 // indirect
@ -79,6 +83,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
github.com/stretchr/objx v0.5.0 // indirect
@ -86,7 +91,9 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.9.0 // indirect

14
go.sum
View File

@ -28,6 +28,8 @@ github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
@ -89,6 +91,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@ -190,6 +194,8 @@ github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZ
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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.2.0 h1:hyK7yPFndU3LCDwEQJwPQUCjNkp1DGP/VxyzrWfXZUU=
@ -242,6 +248,12 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xhit/go-simple-mail/v2 v2.13.0 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI=
github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/yeqown/go-qrcode/v2 v2.2.1 h1:Jc1Q916fwC05R8C7mpWDbrT9tyLPaLLKDABoC5XBCe8=
github.com/yeqown/go-qrcode/v2 v2.2.1/go.mod h1:2Qsk2APUCPne0TsRo40DIkI5MYnbzYKCnKGEFWrxd24=
github.com/yeqown/go-qrcode/writer/standard v1.2.1 h1:FMRZiur5yApUIe4fqtqmcdl/XQTZAZWt2DhkPx4VIW0=
github.com/yeqown/go-qrcode/writer/standard v1.2.1/go.mod h1:ZelyDFiVymrauRjUn454iF7bjsabmB1vixkDA5kq2bw=
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
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/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
@ -255,6 +267,8 @@ golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
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.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=

View File

@ -380,7 +380,7 @@ func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domai
func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
var peers []domain.Peer
err := r.db.WithContext(ctx).Where("interface_identifier = ?", id).Find(&peers).Error
err := r.db.WithContext(ctx).Preload("Addresses").Where("interface_identifier = ?", id).Find(&peers).Error
if err != nil {
return nil, err
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@
let WGPORTAL_BACKEND_BASE_URL="http://localhost:5000/api/v0";
</script>
<script src="/api/v0/config/frontend.js"></script>
<script type="module" crossorigin src="/app/assets/index-2321088d.js"></script>
<script type="module" crossorigin src="/app/assets/index-8f53e6dd.js"></script>
<link rel="stylesheet" href="/app/assets/index-a233ff7e.css">
</head>
<body class="d-flex flex-column min-vh-100">

View File

@ -0,0 +1,15 @@
package handlers
import (
"encoding/base64"
"strings"
)
func Base64UrlDecode(in string) string {
in = strings.ReplaceAll(in, "-", "=")
in = strings.ReplaceAll(in, "_", "/")
in = strings.ReplaceAll(in, ".", "+")
output, _ := base64.StdEncoding.DecodeString(in)
return string(output)
}

View File

@ -90,7 +90,7 @@ func (e interfaceEndpoint) handleAllGet() gin.HandlerFunc {
// @Router /interface/get/{id} [get]
func (e interfaceEndpoint) handleSingleGet() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
id := Base64UrlDecode(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, model.Error{
Code: http.StatusInternalServerError, Message: "missing id parameter",
@ -122,7 +122,7 @@ func (e interfaceEndpoint) handleSingleGet() gin.HandlerFunc {
// @Router /interface/config/{id} [get]
func (e interfaceEndpoint) handleConfigGet() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
id := Base64UrlDecode(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, model.Error{
Code: http.StatusInternalServerError, Message: "missing id parameter",
@ -166,7 +166,7 @@ func (e interfaceEndpoint) handleUpdatePut() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
id := c.Param("id")
id := Base64UrlDecode(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
return
@ -241,7 +241,9 @@ func (e interfaceEndpoint) handleCreatePost() gin.HandlerFunc {
// @Router /interface/peers/{id} [get]
func (e interfaceEndpoint) handlePeersGet() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
ctx := domain.SetUserInfoFromGin(c)
id := Base64UrlDecode(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, model.Error{
Code: http.StatusInternalServerError, Message: "missing id parameter",
@ -249,7 +251,7 @@ func (e interfaceEndpoint) handlePeersGet() gin.HandlerFunc {
return
}
_, peers, err := e.app.GetInterfaceAndPeers(c.Request.Context(), domain.InterfaceIdentifier(id))
_, peers, err := e.app.GetInterfaceAndPeers(ctx, domain.InterfaceIdentifier(id))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{
Code: http.StatusInternalServerError, Message: err.Error(),
@ -276,7 +278,7 @@ func (e interfaceEndpoint) handleDelete() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
id := c.Param("id")
id := Base64UrlDecode(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
return

View File

@ -5,6 +5,7 @@ import (
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/app/api/v0/model"
"github.com/h44z/wg-portal/internal/domain"
"io"
"net/http"
)
@ -23,6 +24,8 @@ func (e peerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
apiGroup.GET("/iface/:iface/all", e.handleAllGet())
apiGroup.GET("/iface/:iface/prepare", e.handlePrepareGet())
apiGroup.POST("/iface/:iface/new", e.handleCreatePost())
apiGroup.GET("/config-qr/:id", e.handleQrCodeGet())
apiGroup.GET("/config/:id", e.handleConfigGet())
apiGroup.GET("/:id", e.handleSingleGet())
apiGroup.PUT("/:id", e.handleUpdatePut())
apiGroup.DELETE("/:id", e.handleDelete())
@ -43,7 +46,7 @@ func (e peerEndpoint) handleAllGet() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
interfaceId := c.Param("iface")
interfaceId := Base64UrlDecode(c.Param("iface"))
if interfaceId == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing iface parameter"})
return
@ -74,7 +77,7 @@ func (e peerEndpoint) handleSingleGet() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
peerId := c.Param("id")
peerId := Base64UrlDecode(c.Param("id"))
if peerId == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing id parameter"})
return
@ -105,7 +108,7 @@ func (e peerEndpoint) handlePrepareGet() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
interfaceId := c.Param("iface")
interfaceId := Base64UrlDecode(c.Param("iface"))
if interfaceId == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing iface parameter"})
return
@ -137,7 +140,7 @@ func (e peerEndpoint) handleCreatePost() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
interfaceId := c.Param("iface")
interfaceId := Base64UrlDecode(c.Param("iface"))
if interfaceId == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing iface parameter"})
return
@ -176,12 +179,12 @@ func (e peerEndpoint) handleCreatePost() gin.HandlerFunc {
// @Success 200 {object} model.Peer
// @Failure 400 {object} model.Error
// @Failure 500 {object} model.Error
// @Router /peer/{id} [post]
// @Router /peer/{id} [put]
func (e peerEndpoint) handleUpdatePut() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
peerId := c.Param("id")
peerId := Base64UrlDecode(c.Param("id"))
if peerId == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing id parameter"})
return
@ -224,7 +227,7 @@ func (e peerEndpoint) handleDelete() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
id := c.Param("id")
id := Base64UrlDecode(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
return
@ -239,3 +242,83 @@ func (e peerEndpoint) handleDelete() gin.HandlerFunc {
c.Status(http.StatusNoContent)
}
}
// handleConfigGet returns a gorm handler function.
//
// @ID peers_handleConfigGet
// @Tags Peer
// @Summary Get peer configuration as string.
// @Produce json
// @Success 200 {object} string
// @Failure 400 {object} model.Error
// @Failure 500 {object} model.Error
// @Router /peer/config/{id} [get]
func (e peerEndpoint) handleConfigGet() gin.HandlerFunc {
return func(c *gin.Context) {
id := Base64UrlDecode(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, model.Error{
Code: http.StatusInternalServerError, Message: "missing id parameter",
})
return
}
config, err := e.app.GetPeerConfig(c.Request.Context(), domain.PeerIdentifier(id))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{
Code: http.StatusInternalServerError, Message: err.Error(),
})
return
}
configString, err := io.ReadAll(config)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{
Code: http.StatusInternalServerError, Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, string(configString))
}
}
// handleQrCodeGet returns a gorm handler function.
//
// @ID peers_handleQrCodeGet
// @Tags Peer
// @Summary Get peer configuration as qr code.
// @Produce json
// @Success 200 {object} string
// @Failure 400 {object} model.Error
// @Failure 500 {object} model.Error
// @Router /peer/config-qr/{id} [get]
func (e peerEndpoint) handleQrCodeGet() gin.HandlerFunc {
return func(c *gin.Context) {
id := Base64UrlDecode(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, model.Error{
Code: http.StatusInternalServerError, Message: "missing id parameter",
})
return
}
config, err := e.app.GetPeerConfigQrCode(c.Request.Context(), domain.PeerIdentifier(id))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{
Code: http.StatusInternalServerError, Message: err.Error(),
})
return
}
configData, err := io.ReadAll(config)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{
Code: http.StatusInternalServerError, Message: err.Error(),
})
return
}
c.Data(http.StatusOK, "image/png", configData)
}
}

View File

@ -39,7 +39,9 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
// @Router /user/all [get]
func (e userEndpoint) handleAllGet() gin.HandlerFunc {
return func(c *gin.Context) {
users, err := e.app.GetAllUsers(c.Request.Context())
ctx := domain.SetUserInfoFromGin(c)
users, err := e.app.GetAllUsers(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
@ -63,7 +65,7 @@ func (e userEndpoint) handleSingleGet() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
id := c.Param("id")
id := Base64UrlDecode(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing user id"})
return
@ -95,7 +97,7 @@ func (e userEndpoint) handleUpdatePut() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
id := c.Param("id")
id := Base64UrlDecode(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing user id"})
return
@ -166,13 +168,15 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
// @Router /user/{id}/peers [get]
func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
return func(c *gin.Context) {
interfaceId := c.Param("id")
ctx := domain.SetUserInfoFromGin(c)
interfaceId := Base64UrlDecode(c.Param("id"))
if interfaceId == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
return
}
peers, err := e.app.GetUserPeers(c.Request.Context(), domain.UserIdentifier(interfaceId))
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(interfaceId))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
@ -197,7 +201,7 @@ func (e userEndpoint) handleDelete() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := domain.SetUserInfoFromGin(c)
id := c.Param("id")
id := Base64UrlDecode(c.Param("id"))
if id == "" {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing user id"})
return

View File

@ -1,10 +1,13 @@
package filetemplate
import (
"bytes"
"context"
"fmt"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/yeqown/go-qrcode/v2"
"github.com/yeqown/go-qrcode/writer/standard"
"io"
)
@ -50,3 +53,41 @@ func (m Manager) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (i
return m.tplHandler.GetPeerConfig(peer)
}
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)
}
cfgData, err := m.tplHandler.GetPeerConfig(peer)
if err != nil {
return nil, fmt.Errorf("failed to get peer config for %s: %w", id, err)
}
configBytes, err := io.ReadAll(cfgData)
if err != nil {
return nil, fmt.Errorf("failed to read peer config for %s: %w", id, err)
}
code, err := qrcode.New(string(configBytes))
if err != nil {
return nil, fmt.Errorf("failed to initializeqr code for %s: %w", id, err)
}
buf := bytes.NewBuffer(nil)
wr := nopCloser{Writer: buf}
qrWriter := standard.NewWithWriter(wr, standard.WithQRWidth(40), standard.WithBuiltinImageEncoder(standard.PNG_FORMAT))
err = code.Save(qrWriter)
if err != nil {
return nil, fmt.Errorf("failed to write code for %s: %w", id, err)
}
return buf, nil
}
type nopCloser struct {
io.Writer
}
func (nopCloser) Close() error { return nil }

View File

@ -1,11 +1,16 @@
# AUTOGENERATED FILE - DO NOT EDIT
# This file uses wg-quick format. See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION
# This file uses wg-quick format.
# See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION
# Lines starting with the -WGP- tag are used by
# the WireGuard Portal configuration parser.
# -WGP- WIREGUARD PORTAL CONFIGURATION FILE, version {{ .Portal.Version }}
# Lines starting with the -WGP- tag are used by the WireGuard Portal configuration parser.
# -WGP- WIREGUARD PORTAL CONFIGURATION FILE
# -WGP- version {{ .Portal.Version }}
[Interface]
# -WGP- Interface: {{ .Interface.Identifier }} | Updated: {{ .Interface.UpdatedAt }} | Created: {{ .Interface.CreatedAt }}
# -WGP- Interface: {{ .Interface.Identifier }}
# -WGP- Created: {{ .Interface.CreatedAt }}
# -WGP- Updated: {{ .Interface.UpdatedAt }}
# -WGP- Display name: {{ .Interface.DisplayName }}
# -WGP- Interface mode: {{ .Interface.Type }}
# -WGP- PublicKey = {{ .Interface.KeyPair.PublicKey }}
@ -53,30 +58,32 @@ PostDown = {{ .Interface.PostDown }}
#
{{range .Peers}}
{{- if not .DisabledAt}}
{{- if not .IsDisabled}}
[Peer]
# -WGP- Peer: {{.Uid}} | Updated: {{.UpdatedAt}} | Created: {{.CreatedAt}}
# -WGP- Display name: {{ .Identifier }}
{{- if .KeyPair.PrivateKey}}
# -WGP- PrivateKey: {{.KeyPair.PrivateKey}}
# -WGP- Peer: {{.Identifier}}
# -WGP- Created: {{.CreatedAt}}
# -WGP- Updated: {{.UpdatedAt}}
# -WGP- Display name: {{ .DisplayName }}
{{- if .Interface.KeyPair.PrivateKey}}
# -WGP- PrivateKey: {{.Interface.KeyPair.PrivateKey}}
{{- end}}
PublicKey = {{ .KeyPair.PublicKey }}
PublicKey = {{ .Interface.KeyPair.PublicKey }}
{{- if .PresharedKey}}
PresharedKey = {{ .PresharedKey }}
{{- end}}
{{- if eq $.Interface.Type "server"}}
AllowedIPs = {{ .AddressStr }}{{if ne .ExtraAllowedIPsStr ""}}, {{ .ExtraAllowedIPsStr }}{{end}}
AllowedIPs = {{ CidrsToString .Interface.Addresses }}{{if ne .ExtraAllowedIPsStr ""}}, {{ .ExtraAllowedIPsStr }}{{end}}
{{- end}}
{{- if eq $.Interface.Type "client"}}
{{- if .AllowedIPsStr}}
AllowedIPs = {{ .AllowedIPsStr }}
{{- if .AllowedIPsStr.GetValue}}
AllowedIPs = {{ .AllowedIPsStr.GetValue }}
{{- end}}
{{- end}}
{{- if and (ne .Endpoint "") (eq $.Interface.Type "client")}}
Endpoint = {{ .Endpoint }}
{{- if and (ne .Endpoint.GetValue "") (eq $.Interface.Type "client")}}
Endpoint = {{ .Endpoint.GetValue }}
{{- end}}
{{- if ne .PersistentKeepalive 0}}
PersistentKeepalive = {{ .PersistentKeepalive }}
{{- if and (ne .PersistentKeepalive.GetValue 0) (eq $.Interface.Type "client")}}
PersistentKeepalive = {{ .PersistentKeepalive.GetValue }}
{{- end}}
{{- end}}
{{end}}

View File

@ -1,22 +1,27 @@
# AUTOGENERATED FILE - DO NOT EDIT
# This file uses wg-quick format. See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION
# This file uses wg-quick format.
# See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION
# Lines starting with the -WGP- tag are used by
# the WireGuard Portal configuration parser.
# -WGP- WIREGUARD PORTAL CONFIGURATION FILE, version {{ .Portal.Version }}
# Lines starting with the -WGP- tag are used by the WireGuard Portal configuration parser.
# -WGP- WIREGUARD PORTAL CONFIGURATION FILE
# -WGP- version {{ .Portal.Version }}
[Interface]
# -WGP- Peer: {{.Peer.Identifier}} | Updated: {{.Peer.UpdatedAt}} | Created: {{.Peer.CreatedAt}}
# -WGP- Peer: {{.Peer.Identifier}}
# -WGP- Created: {{.Peer.CreatedAt}}
# -WGP- Updated: {{.Peer.UpdatedAt}}
# -WGP- Display name: {{ .Peer.DisplayName }}
# -WGP- PublicKey: {{ .Peer.KeyPair.PublicKey }}
# -WGP- PublicKey: {{ .Peer.Interface.KeyPair.PublicKey }}
{{- if eq .Peer.Interface.Type "server"}}
# -WGP- Peer type: client
{{else}}
# -WGP- Peer type: server
{{else}}
# -WGP- Peer type: client
{{- end}}
# Core settings
PrivateKey = {{ .Peer.KeyPair.PrivateKey }}
Address = {{ .Peer.Interface.AddressStr }}
PrivateKey = {{ .Peer.Interface.KeyPair.PrivateKey }}
Address = {{ CidrsToString .Peer.Interface.Addresses }}
# Misc. settings (optional)
{{- if .Peer.Interface.DnsStr.GetValue}}
@ -47,7 +52,7 @@ PostDown = {{ .Peer.Interface.PostDown.GetValue }}
{{- end}}
[Peer]
PublicKey = {{ .Peer.Interface.PublicKey }}
PublicKey = {{ .Peer.EndpointPublicKey.GetValue }}
Endpoint = {{ .Peer.Endpoint.GetValue }}
{{- if .Peer.AllowedIPsStr.GetValue}}
AllowedIPs = {{ .Peer.AllowedIPsStr.GetValue }}
@ -55,6 +60,6 @@ AllowedIPs = {{ .Peer.AllowedIPsStr.GetValue }}
{{- if .Peer.PresharedKey}}
PresharedKey = {{ .Peer.PresharedKey }}
{{- end}}
{{- if ne .Peer.PersistentKeepalive.GetValue 0}}
{{- if and (ne .Peer.PersistentKeepalive.GetValue 0) (eq .Peer.Interface.Type "client")}}
PersistentKeepalive = {{ .Peer.PersistentKeepalive.GetValue }}
{{- end}}

View File

@ -51,4 +51,5 @@ type StatisticsCollector interface {
type TemplateManager interface {
GetInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) (io.Reader, error)
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
}