mirror of
https://github.com/h44z/wg-portal.git
synced 2025-08-12 16:22:23 +00:00
global peer defaults, many more improvements
This commit is contained in:
parent
2235c2a0d7
commit
5ec2ad6827
@ -9,10 +9,12 @@ import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
|||||||
import isCidr from "is-cidr";
|
import isCidr from "is-cidr";
|
||||||
import {isIP} from 'is-ip';
|
import {isIP} from 'is-ip';
|
||||||
import { freshInterface } from '@/helpers/models';
|
import { freshInterface } from '@/helpers/models';
|
||||||
|
import {peerStore} from "@/stores/peers";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const interfaces = interfaceStore()
|
const interfaces = interfaceStore()
|
||||||
|
const peers = peerStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
interfaceId: String,
|
interfaceId: String,
|
||||||
@ -243,6 +245,31 @@ async function save() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyPeerDefaults() {
|
||||||
|
if (props.interfaceId==='#NEW#') {
|
||||||
|
return; // do nothing for new interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await interfaces.ApplyPeerDefaults(selectedInterface.value.Identifier, formData.value)
|
||||||
|
|
||||||
|
notify({
|
||||||
|
title: "Peer Defaults Applied",
|
||||||
|
text: "Applied current peer defaults to all available peers.",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
|
||||||
|
await peers.LoadPeers(selectedInterface.value.Identifier) // reload all peers after applying the defaults
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to apply peer defaults!",
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function del() {
|
async function del() {
|
||||||
try {
|
try {
|
||||||
await interfaces.DeleteInterface(selectedInterface.value.Identifier)
|
await interfaces.DeleteInterface(selectedInterface.value.Identifier)
|
||||||
@ -312,11 +339,11 @@ async function del() {
|
|||||||
:validate="validateCIDR"
|
:validate="validateCIDR"
|
||||||
@on-tags-changed="handleChangeAddresses"/>
|
@on-tags-changed="handleChangeAddresses"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="formData.Type==='server'" class="form-group">
|
<div v-if="formData.Mode==='server'" class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interfaceedit.listenport') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interfaceedit.listenport') }}</label>
|
||||||
<input v-model="formData.ListenPort" class="form-control" placeholder="Listen Port" type="text">
|
<input v-model="formData.ListenPort" class="form-control" placeholder="Listen Port" type="number">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interfaceedit.dns') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interfaceedit.dns') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.Dns"
|
<vue3-tags-input class="form-control" :tags="formData.Dns"
|
||||||
placeholder="DNS Servers"
|
placeholder="DNS Servers"
|
||||||
@ -324,7 +351,7 @@ async function del() {
|
|||||||
:validate="validateIP"
|
:validate="validateIP"
|
||||||
@on-tags-changed="handleChangeDns"/>
|
@on-tags-changed="handleChangeDns"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interfaceedit.dnssearch') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interfaceedit.dnssearch') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch"
|
<vue3-tags-input class="form-control" :tags="formData.DnsSearch"
|
||||||
placeholder="DNS Search prefixes"
|
placeholder="DNS Search prefixes"
|
||||||
@ -463,6 +490,10 @@ async function del() {
|
|||||||
<textarea v-model="formData.PeerDefPostDown" class="form-control" rows="2"></textarea>
|
<textarea v-model="formData.PeerDefPostDown" class="form-control" rows="2"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset v-if="props.interfaceId!=='#NEW#'" class="text-end">
|
||||||
|
<hr class="mt-4">
|
||||||
|
<button class="btn btn-primary me-1" type="button" @click.prevent="applyPeerDefaults">Apply Peer Defaults</button>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -132,11 +132,43 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
|||||||
formData.value.PreDown = selectedPeer.value.PreDown
|
formData.value.PreDown = selectedPeer.value.PreDown
|
||||||
formData.value.PostDown = selectedPeer.value.PostDown
|
formData.value.PostDown = selectedPeer.value.PostDown
|
||||||
|
|
||||||
|
if (!formData.value.Endpoint.Overridable ||
|
||||||
|
!formData.value.EndpointPublicKey.Overridable ||
|
||||||
|
!formData.value.AllowedIPs.Overridable ||
|
||||||
|
!formData.value.PersistentKeepalive.Overridable ||
|
||||||
|
!formData.value.Dns.Overridable ||
|
||||||
|
!formData.value.DnsSearch.Overridable ||
|
||||||
|
!formData.value.Mtu.Overridable ||
|
||||||
|
!formData.value.FirewallMark.Overridable ||
|
||||||
|
!formData.value.RoutingTable.Overridable ||
|
||||||
|
!formData.value.PreUp.Overridable ||
|
||||||
|
!formData.value.PostUp.Overridable ||
|
||||||
|
!formData.value.PreDown.Overridable ||
|
||||||
|
!formData.value.PostDown.Overridable) {
|
||||||
|
formData.value.IgnoreGlobalSettings = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(() => formData.value.IgnoreGlobalSettings, async (newValue, oldValue) => {
|
||||||
|
formData.value.Endpoint.Overridable = !newValue
|
||||||
|
formData.value.EndpointPublicKey.Overridable = !newValue
|
||||||
|
formData.value.AllowedIPs.Overridable = !newValue
|
||||||
|
formData.value.PersistentKeepalive.Overridable = !newValue
|
||||||
|
formData.value.Dns.Overridable = !newValue
|
||||||
|
formData.value.DnsSearch.Overridable = !newValue
|
||||||
|
formData.value.Mtu.Overridable = !newValue
|
||||||
|
formData.value.FirewallMark.Overridable = !newValue
|
||||||
|
formData.value.RoutingTable.Overridable = !newValue
|
||||||
|
formData.value.PreUp.Overridable = !newValue
|
||||||
|
formData.value.PostUp.Overridable = !newValue
|
||||||
|
formData.value.PreDown.Overridable = !newValue
|
||||||
|
formData.value.PostDown.Overridable = !newValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
formData.value = freshPeer()
|
formData.value = freshPeer()
|
||||||
emit('close')
|
emit('close')
|
||||||
@ -346,6 +378,10 @@ async function del() {
|
|||||||
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
|
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
|
||||||
<label class="form-check-label" >Disabled</label>
|
<label class="form-check-label" >Disabled</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" v-model="formData.IgnoreGlobalSettings">
|
||||||
|
<label class="form-check-label">Ignore global settings</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
<label class="form-label">{{ $t('modals.peeredit.expiresat') }}</label>
|
<label class="form-label">{{ $t('modals.peeredit.expiresat') }}</label>
|
||||||
|
@ -131,8 +131,9 @@ function email() {
|
|||||||
<li>Identifier: {{ selectedPeer.PublicKey }}</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>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>Linked User: {{ selectedPeer.UserIdentifier }}</li>
|
||||||
<li>Notes: {{ selectedPeer.Notes }}</li>
|
<li v-if="selectedPeer.Notes">Notes: {{ selectedPeer.Notes }}</li>
|
||||||
<li>Expires At: {{ selectedPeer.ExpiresAt }}</li>
|
<li v-if="selectedPeer.ExpiresAt">Expires At: {{ selectedPeer.ExpiresAt }}</li>
|
||||||
|
<li v-if="selectedPeer.Disabled">Disabled Reason: {{ selectedPeer.DisabledReason }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
|
@ -115,7 +115,10 @@ export function freshPeer() {
|
|||||||
PostDown: {
|
PostDown: {
|
||||||
Value: "",
|
Value: "",
|
||||||
Overridable: true,
|
Overridable: true,
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// Internal value
|
||||||
|
IgnoreGlobalSettings: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,9 @@
|
|||||||
"firstname": "Firstname",
|
"firstname": "Firstname",
|
||||||
"lastname": "Lastname",
|
"lastname": "Lastname",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"lang": "Toggle Language"
|
"lang": "Toggle Language",
|
||||||
|
"profile": "My Profile",
|
||||||
|
"logout": "Logout"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"h1": "WireGuard® VPN Portal",
|
"h1": "WireGuard® VPN Portal",
|
||||||
@ -49,13 +51,13 @@
|
|||||||
"h1": "Interface Administration",
|
"h1": "Interface Administration",
|
||||||
"h2": "Current VPN Peers",
|
"h2": "Current VPN Peers",
|
||||||
"h2-client": "Current Endpoints",
|
"h2-client": "Current Endpoints",
|
||||||
"tableHeadings": [
|
"tableHeadings": {
|
||||||
"Name",
|
"name": "Name",
|
||||||
"User",
|
"user": "User",
|
||||||
"IP's",
|
"ip": "IP's",
|
||||||
"Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"Handshake"
|
"stats": "Status"
|
||||||
],
|
},
|
||||||
"noInterface": {
|
"noInterface": {
|
||||||
"h1": "No interfaces found...",
|
"h1": "No interfaces found...",
|
||||||
"message": "Click the plus button above to create a new WireGuard interface."
|
"message": "Click the plus button above to create a new WireGuard interface."
|
||||||
@ -79,10 +81,6 @@
|
|||||||
"h4": "No peers for the selected interface...",
|
"h4": "No peers for the selected interface...",
|
||||||
"message": "Click the plus button above to create a new WireGuard interface."
|
"message": "Click the plus button above to create a new WireGuard interface."
|
||||||
},
|
},
|
||||||
"pagination": {
|
|
||||||
"size": "Number of Elements",
|
|
||||||
"all": "All (slow)"
|
|
||||||
},
|
|
||||||
"peer": {
|
"peer": {
|
||||||
"new": "Create new peer",
|
"new": "Create new peer",
|
||||||
"edit": "Edit peer"
|
"edit": "Edit peer"
|
||||||
@ -116,9 +114,24 @@
|
|||||||
"addUser": "Add User",
|
"addUser": "Add User",
|
||||||
"addMulti": "Add Multiple Users"
|
"addMulti": "Add Multiple Users"
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"h2-clients": "My VPN Peers",
|
||||||
|
"tableHeadings": {
|
||||||
|
"name": "Name",
|
||||||
|
"ip": "IP's",
|
||||||
|
"stats": "Status",
|
||||||
|
"interface": "Server Interface"
|
||||||
|
}
|
||||||
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"peeredit": {
|
"peeredit": {
|
||||||
"privatekey": "Private Key"
|
"privatekey": "Private Key"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"pagination": {
|
||||||
|
"size": "Number of Elements",
|
||||||
|
"all": "All (slow)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,4 +32,15 @@ app.use(createPinia());
|
|||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(Notifications);
|
app.use(Notifications);
|
||||||
|
|
||||||
|
app.config.globalProperties.$filters = {
|
||||||
|
truncate(value, maxLength, suffix) {
|
||||||
|
suffix = suffix || '...'
|
||||||
|
if (value.length > maxLength) {
|
||||||
|
return value.substring(0, maxLength) + suffix;
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
@ -123,6 +123,30 @@ export const interfaceStore = defineStore({
|
|||||||
console.log(error)
|
console.log(error)
|
||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
async ApplyPeerDefaults(id, formData) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/apply-peer-defaults`, formData)
|
||||||
|
.then(() => {
|
||||||
|
this.fetching = false
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.fetching = false
|
||||||
|
console.log(error)
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async SaveConfiguration(id) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/save-config`)
|
||||||
|
.then(() => {
|
||||||
|
this.fetching = false
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.fetching = false
|
||||||
|
console.log(error)
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -43,7 +43,7 @@ export const peerStore = defineStore({
|
|||||||
return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
|
return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
|
||||||
},
|
},
|
||||||
ConfigQrUrl: (state) => {
|
ConfigQrUrl: (state) => {
|
||||||
return (id) => apiWrapper.url(`${baseUrl}/config-qr/${base64_url_encode(id)}`)
|
return (id) => state.peers.find((p) => p.Identifier === id) ? apiWrapper.url(`${baseUrl}/config-qr/${base64_url_encode(id)}`) : ''
|
||||||
},
|
},
|
||||||
isFetching: (state) => state.fetching,
|
isFetching: (state) => state.fetching,
|
||||||
hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
|
hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
|
||||||
|
@ -3,7 +3,7 @@ import {apiWrapper} from "@/helpers/fetch-wrapper";
|
|||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
import {authStore} from "@/stores/auth";
|
import {authStore} from "@/stores/auth";
|
||||||
import { base64_url_encode } from '@/helpers/encoding';
|
import { base64_url_encode } from '@/helpers/encoding';
|
||||||
|
import {freshStats} from "@/helpers/models";
|
||||||
|
|
||||||
const baseUrl = `/user`
|
const baseUrl = `/user`
|
||||||
|
|
||||||
@ -11,6 +11,8 @@ export const profileStore = defineStore({
|
|||||||
id: 'profile',
|
id: 'profile',
|
||||||
state: () => ({
|
state: () => ({
|
||||||
peers: [],
|
peers: [],
|
||||||
|
stats: {},
|
||||||
|
statsEnabled: false,
|
||||||
user: {},
|
user: {},
|
||||||
filter: "",
|
filter: "",
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@ -40,6 +42,10 @@ export const profileStore = defineStore({
|
|||||||
hasNextPage: (state) => state.pageOffset < (state.FilteredPeerCount - state.pageSize),
|
hasNextPage: (state) => state.pageOffset < (state.FilteredPeerCount - state.pageSize),
|
||||||
hasPrevPage: (state) => state.pageOffset > 0,
|
hasPrevPage: (state) => state.pageOffset > 0,
|
||||||
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
|
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
|
||||||
|
Statistics: (state) => {
|
||||||
|
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
||||||
|
},
|
||||||
|
hasStatistics: (state) => state.statsEnabled,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
afterPageSizeChange() {
|
afterPageSizeChange() {
|
||||||
@ -77,6 +83,14 @@ export const profileStore = defineStore({
|
|||||||
this.user = user
|
this.user = user
|
||||||
this.fetching = false
|
this.fetching = false
|
||||||
},
|
},
|
||||||
|
setStats(statsResponse) {
|
||||||
|
if (!statsResponse) {
|
||||||
|
this.stats = {}
|
||||||
|
this.statsEnabled = false
|
||||||
|
}
|
||||||
|
this.stats = statsResponse.Stats
|
||||||
|
this.statsEnabled = statsResponse.Enabled
|
||||||
|
},
|
||||||
async LoadPeers() {
|
async LoadPeers() {
|
||||||
this.fetching = true
|
this.fetching = true
|
||||||
let currentUser = authStore().user.Identifier
|
let currentUser = authStore().user.Identifier
|
||||||
@ -91,6 +105,20 @@ export const profileStore = defineStore({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async LoadStats() {
|
||||||
|
this.fetching = true
|
||||||
|
let currentUser = authStore().user.Identifier
|
||||||
|
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}/stats`)
|
||||||
|
.then(this.setStats)
|
||||||
|
.catch(error => {
|
||||||
|
this.setStats(undefined)
|
||||||
|
console.log("Failed to load peer stats: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to load peer stats!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
async LoadUser() {
|
async LoadUser() {
|
||||||
this.fetching = true
|
this.fetching = true
|
||||||
let currentUser = authStore().user.Identifier
|
let currentUser = authStore().user.Identifier
|
||||||
|
@ -8,6 +8,7 @@ import InterfaceViewModal from "../components/InterfaceViewModal.vue";
|
|||||||
import {onMounted, ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
import {peerStore} from "../stores/peers";
|
import {peerStore} from "../stores/peers";
|
||||||
import {interfaceStore} from "../stores/interfaces";
|
import {interfaceStore} from "../stores/interfaces";
|
||||||
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
const interfaces = interfaceStore()
|
const interfaces = interfaceStore()
|
||||||
const peers = peerStore()
|
const peers = peerStore()
|
||||||
@ -44,6 +45,25 @@ async function download() {
|
|||||||
document.body.removeChild(element)
|
document.body.removeChild(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
try {
|
||||||
|
await interfaces.SaveConfiguration(interfaces.GetSelected.Identifier)
|
||||||
|
|
||||||
|
notify({
|
||||||
|
title: "Interface configuration persisted to file",
|
||||||
|
text: "The interface configuration has been written to the wg-quick configuration file.",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to persist interface configuration file!",
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await interfaces.LoadInterfaces()
|
await interfaces.LoadInterfaces()
|
||||||
await peers.LoadPeers(undefined) // use default interface
|
await peers.LoadPeers(undefined) // use default interface
|
||||||
@ -99,11 +119,12 @@ onMounted(async () => {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-lg-8">
|
<div class="col-12 col-lg-8">
|
||||||
{{ $t('interfaces.statusBox.h1') }} <strong>{{interfaces.GetSelected.Identifier}}</strong> ({{interfaces.GetSelected.Mode}} {{ $t('interfaces.statusBox.mode') }})
|
{{ $t('interfaces.statusBox.h1') }} <strong>{{interfaces.GetSelected.Identifier}}</strong> ({{interfaces.GetSelected.Mode}} {{ $t('interfaces.statusBox.mode') }})
|
||||||
|
<span v-if="interfaces.GetSelected.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="interfaces.GetSelected.DisabledReason"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-4 text-lg-end">
|
<div class="col-12 col-lg-4 text-lg-end">
|
||||||
<a class="btn-link" href="#" title="Show interface configuration" @click.prevent="viewedInterfaceId=interfaces.GetSelected.Identifier"><i class="fas fa-eye"></i></a>
|
<a class="btn-link" href="#" title="Show interface configuration" @click.prevent="viewedInterfaceId=interfaces.GetSelected.Identifier"><i class="fas fa-eye"></i></a>
|
||||||
<a class="ms-5 btn-link" href="#" title="Download interface configuration" @click.prevent="download"><i class="fas fa-download"></i></a>
|
<a class="ms-5 btn-link" href="#" title="Download interface configuration" @click.prevent="download"><i class="fas fa-download"></i></a>
|
||||||
<a class="ms-5 btn-link" href="#" title="Write interface configuration file"><i class="fas fa-save"></i></a>
|
<a class="ms-5 btn-link" href="#" title="Write interface configuration file" @click.prevent="saveConfig"><i class="fas fa-save"></i></a>
|
||||||
<a class="ms-5 btn-link" href="#" title="Edit interface settings" @click.prevent="editInterfaceId=interfaces.GetSelected.Identifier"><i class="fas fa-cog"></i></a>
|
<a class="ms-5 btn-link" href="#" title="Edit interface settings" @click.prevent="editInterfaceId=interfaces.GetSelected.Identifier"><i class="fas fa-cog"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -152,7 +173,7 @@ onMounted(async () => {
|
|||||||
<td>{{interfaces.GetSelected.Mtu}}</td>
|
<td>{{interfaces.GetSelected.Mtu}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $t('interfaces.statusBox.intervall') }}:</td>
|
<td>{{ $t('interfaces.statusBox.interval') }}:</td>
|
||||||
<td>{{interfaces.GetSelected.PeerDefPersistentKeepalive}}</td>
|
<td>{{interfaces.GetSelected.PeerDefPersistentKeepalive}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@ -248,7 +269,7 @@ onMounted(async () => {
|
|||||||
<td>{{interfaces.GetSelected.Mtu}}</td>
|
<td>{{interfaces.GetSelected.Mtu}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $t('interfaces.statusBox.intervall') }}:</td>
|
<td>{{ $t('interfaces.statusBox.interval') }}:</td>
|
||||||
<td>{{interfaces.GetSelected.PeerDefPersistentKeepalive}}</td>
|
<td>{{interfaces.GetSelected.PeerDefPersistentKeepalive}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -291,11 +312,12 @@ onMounted(async () => {
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
<input id="flexCheckDefault" class="form-check-input" title="Select all" type="checkbox" value="">
|
<input id="flexCheckDefault" class="form-check-input" title="Select all" type="checkbox" value="">
|
||||||
</th><!-- select -->
|
</th><!-- select -->
|
||||||
<th scope="col">{{ $t('interfaces.tableHeadings[0]') }}</th>
|
<th scope="col"></th><!-- status -->
|
||||||
<th scope="col">{{ $t('interfaces.tableHeadings[1]') }}</th>
|
<th scope="col">{{ $t('interfaces.tableHeadings.name') }}</th>
|
||||||
<th scope="col">{{ $t('interfaces.tableHeadings[2]') }}</th>
|
<th scope="col">{{ $t('interfaces.tableHeadings.user') }}</th>
|
||||||
<th v-if="interfaces.GetSelected.Mode==='client'" scope="col">{{ $t('interfaces.tableHeadings[3]') }}</th>
|
<th scope="col">{{ $t('interfaces.tableHeadings.ip') }}</th>
|
||||||
<th v-if="peers.hasStatistics" scope="col">{{ $t('interfaces.tableHeadings[4]') }}</th>
|
<th v-if="interfaces.GetSelected.Mode==='client'" scope="col">{{ $t('interfaces.tableHeadings.endpoint') }}</th>
|
||||||
|
<th v-if="peers.hasStatistics" scope="col">{{ $t('interfaces.tableHeadings.stats') }}</th>
|
||||||
<th scope="col"></th><!-- Actions -->
|
<th scope="col"></th><!-- Actions -->
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -304,7 +326,11 @@ onMounted(async () => {
|
|||||||
<th scope="row">
|
<th scope="row">
|
||||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
||||||
</th>
|
</th>
|
||||||
<td>{{peer.DisplayName}}</td>
|
<td class="text-center">
|
||||||
|
<span v-if="peer.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="peer.DisabledReason"></i></span>
|
||||||
|
<span v-if="!peer.Disabled && peer.ExpiresAt" class="text-warning"><i class="fas fa-hourglass-end expiring-peer" :title="peer.ExpiresAt"></i></span>
|
||||||
|
</td>
|
||||||
|
<td><span v-if="peer.DisplayName" :title="peer.Identifier">{{peer.DisplayName}}</span><span v-else :title="peer.Identifier">{{ $filters.truncate(peer.Identifier, 10)}}</span></td>
|
||||||
<td>{{peer.UserIdentifier}}</td>
|
<td>{{peer.UserIdentifier}}</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-for="ip in peer.Addresses" :key="ip" class="badge bg-light me-1">{{ ip }}</span>
|
<span v-for="ip in peer.Addresses" :key="ip" class="badge bg-light me-1">{{ ip }}</span>
|
||||||
@ -346,14 +372,14 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('interfaces.pagination.size') }}:</label>
|
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<select v-model.number="peers.pageSize" class="form-select" @click="peers.afterPageSizeChange()">
|
<select v-model.number="peers.pageSize" class="form-select" @click="peers.afterPageSizeChange()">
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
<option value="999999999">{{ $t('interfaces.pagination.all') }}</option>
|
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,7 @@ const editPeerId = ref("")
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await profile.LoadUser()
|
await profile.LoadUser()
|
||||||
await profile.LoadPeers()
|
await profile.LoadPeers()
|
||||||
|
await profile.LoadStats()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -46,11 +47,11 @@ onMounted(async () => {
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
<input id="flexCheckDefault" class="form-check-input" title="Select all" type="checkbox" value="">
|
<input id="flexCheckDefault" class="form-check-input" title="Select all" type="checkbox" value="">
|
||||||
</th><!-- select -->
|
</th><!-- select -->
|
||||||
<th scope="col">{{ $t('profile.tableHeadings[0]') }}</th>
|
<th scope="col"></th><!-- status -->
|
||||||
<th scope="col">{{ $t('profile.tableHeadings[1]') }}</th>
|
<th scope="col">{{ $t('profile.tableHeadings.name') }}</th>
|
||||||
<th scope="col">{{ $t('profile.tableHeadings[2]') }}</th>
|
<th scope="col">{{ $t('profile.tableHeadings.ip') }}</th>
|
||||||
<th scope="col">{{ $t('profile.tableHeadings[3]') }}</th>
|
<th v-if="profile.hasStatistics" scope="col">{{ $t('profile.tableHeadings.stats') }}</th>
|
||||||
<th scope="col">{{ $t('profile.tableHeadings[4]') }}</th>
|
<th scope="col">{{ $t('profile.tableHeadings.interface') }}</th>
|
||||||
<th scope="col"></th><!-- Actions -->
|
<th scope="col"></th><!-- Actions -->
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -59,13 +60,23 @@ onMounted(async () => {
|
|||||||
<th scope="row">
|
<th scope="row">
|
||||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
||||||
</th>
|
</th>
|
||||||
<td>{{peer.DisplayName}}</td>
|
<td class="text-center">
|
||||||
<td>{{peer.Identifier}}</td>
|
<span v-if="peer.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="peer.DisabledReason"></i></span>
|
||||||
<td>{{peer.UserIdentifier}}</td>
|
<span v-if="!peer.Disabled && peer.ExpiresAt" class="text-warning"><i class="fas fa-hourglass-end" :title="peer.ExpiresAt"></i></span>
|
||||||
|
</td>
|
||||||
|
<td><span v-if="peer.DisplayName" :title="peer.Identifier">{{peer.DisplayName}}</span><span v-else :title="peer.Identifier">{{$filters.truncate(peer.Identifier, 10)}}</span></td>
|
||||||
<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 rounded-pill bg-light">{{ ip }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{peer.LastConnected}}</td>
|
<td v-if="profile.hasStatistics">
|
||||||
|
<div v-if="profile.Statistics(peer.Identifier).IsConnected">
|
||||||
|
<span class="badge rounded-pill bg-success"><i class="fa-solid fa-link"></i></span> <span :title="peers.Statistics(peer.Identifier).LastHandshake">Connected</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<span class="badge rounded-pill bg-light"><i class="fa-solid fa-link-slash"></i></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{peer.InterfaceIdentifier}}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="#" title="Show peer" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
<a href="#" title="Show peer" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
||||||
<a href="#" title="Edit peer" @click.prevent="editPeerId=peer.Identifier"><i class="fas fa-cog"></i></a>
|
<a href="#" title="Edit peer" @click.prevent="editPeerId=peer.Identifier"><i class="fas fa-cog"></i></a>
|
||||||
@ -94,14 +105,14 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('profile.pagination.size') }}:</label>
|
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<select v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
|
<select v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
<option value="999999999">{{ $t('profile.pagination.all') }}</option>
|
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -116,14 +116,14 @@ function editUser(user) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('interfaces.pagination.size') }}:</label>
|
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<select v-model.number="users.pageSize" class="form-select" @click="users.afterPageSizeChange()">
|
<select v-model.number="users.pageSize" class="form-select" @click="users.afterPageSizeChange()">
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
<option value="999999999">{{ $t('interfaces.pagination.all') }}</option>
|
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
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
@ -11,7 +11,7 @@
|
|||||||
let WGPORTAL_BACKEND_BASE_URL="http://localhost:5000/api/v0";
|
let WGPORTAL_BACKEND_BASE_URL="http://localhost:5000/api/v0";
|
||||||
</script>
|
</script>
|
||||||
<script src="/api/v0/config/frontend.js"></script>
|
<script src="/api/v0/config/frontend.js"></script>
|
||||||
<script type="module" crossorigin src="/app/assets/index-6aef20f0.js"></script>
|
<script type="module" crossorigin src="/app/assets/index-01f1dd15.js"></script>
|
||||||
<link rel="stylesheet" href="/app/assets/index-a233ff7e.css">
|
<link rel="stylesheet" href="/app/assets/index-a233ff7e.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="d-flex flex-column min-vh-100">
|
<body class="d-flex flex-column min-vh-100">
|
||||||
|
@ -28,6 +28,8 @@ func (e interfaceEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *aut
|
|||||||
apiGroup.DELETE("/:id", e.handleDelete())
|
apiGroup.DELETE("/:id", e.handleDelete())
|
||||||
apiGroup.POST("/new", e.handleCreatePost())
|
apiGroup.POST("/new", e.handleCreatePost())
|
||||||
apiGroup.GET("/config/:id", e.handleConfigGet())
|
apiGroup.GET("/config/:id", e.handleConfigGet())
|
||||||
|
apiGroup.POST("/:id/save-config", e.handleSaveConfigPost())
|
||||||
|
apiGroup.POST("/:id/apply-peer-defaults", e.handleApplyPeerDefaultsPost())
|
||||||
|
|
||||||
apiGroup.GET("/peers/:id", e.handlePeersGet())
|
apiGroup.GET("/peers/:id", e.handlePeersGet())
|
||||||
}
|
}
|
||||||
@ -295,3 +297,76 @@ func (e interfaceEndpoint) handleDelete() gin.HandlerFunc {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleSaveConfigPost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleSaveConfigPost
|
||||||
|
// @Tags Interface
|
||||||
|
// @Summary Save the interface configuration in wg-quick format to a file.
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "The interface identifier"
|
||||||
|
// @Success 204 "No content if saving the configuration was successful"
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /interface/{id}/save-config [post]
|
||||||
|
func (e interfaceEndpoint) handleSaveConfigPost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
//ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := Base64UrlDecode(c.Param("id"))
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleApplyPeerDefaultsPost returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID interfaces_handleApplyPeerDefaultsPost
|
||||||
|
// @Tags Interface
|
||||||
|
// @Summary Apply all peer defaults to the available peers.
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "The interface identifier"
|
||||||
|
// @Param request body model.Interface true "The interface data"
|
||||||
|
// @Success 204 "No content if applying peer defaults was successful"
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /interface/{id}/apply-peer-defaults [post]
|
||||||
|
func (e interfaceEndpoint) handleApplyPeerDefaultsPost() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
id := Base64UrlDecode(c.Param("id"))
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var in model.Interface
|
||||||
|
err := c.BindJSON(&in)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if id != in.Identifier {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "interface id mismatch"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.app.ApplyPeerDefaults(ctx, model.NewDomainInterface(&in))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error{
|
||||||
|
Code: http.StatusInternalServerError, Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -433,6 +433,6 @@ func (e peerEndpoint) handleStatsGet() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.NewPeerStats(true, stats))
|
c.JSON(http.StatusOK, model.NewPeerStats(e.app.Config.Statistics.CollectPeerData, stats))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
|
|||||||
apiGroup.DELETE("/:id", e.handleDelete())
|
apiGroup.DELETE("/:id", e.handleDelete())
|
||||||
apiGroup.POST("/new", e.handleCreatePost())
|
apiGroup.POST("/new", e.handleCreatePost())
|
||||||
apiGroup.GET("/:id/peers", e.handlePeersGet())
|
apiGroup.GET("/:id/peers", e.handlePeersGet())
|
||||||
|
apiGroup.GET("/:id/stats", e.handleStatsGet())
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAllGet returns a gorm handler function.
|
// handleAllGet returns a gorm handler function.
|
||||||
@ -164,6 +165,7 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
|
|||||||
// @Summary Get peers for the given user.
|
// @Summary Get peers for the given user.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} []model.Peer
|
// @Success 200 {object} []model.Peer
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
// @Failure 500 {object} model.Error
|
// @Failure 500 {object} model.Error
|
||||||
// @Router /user/{id}/peers [get]
|
// @Router /user/{id}/peers [get]
|
||||||
func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
|
func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
|
||||||
@ -186,6 +188,36 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleStatsGet returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleStatsGet
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Get peer stats for the given user.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} model.PeerStats
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/{id}/stats [get]
|
||||||
|
func (e userEndpoint) handleStatsGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
userId := Base64UrlDecode(c.Param("id"))
|
||||||
|
if userId == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := e.app.GetUserPeerStats(ctx, domain.UserIdentifier(userId))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.NewPeerStats(e.app.Config.Statistics.CollectPeerData, stats))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// handleDelete returns a gorm handler function.
|
// handleDelete returns a gorm handler function.
|
||||||
//
|
//
|
||||||
// @ID users_handleDelete
|
// @ID users_handleDelete
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package configfile
|
package configfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -8,6 +9,7 @@ import (
|
|||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"github.com/yeqown/go-qrcode/v2"
|
"github.com/yeqown/go-qrcode/v2"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@ -64,12 +66,21 @@ func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifi
|
|||||||
return nil, fmt.Errorf("failed to get peer config for %s: %w", id, err)
|
return nil, fmt.Errorf("failed to get peer config for %s: %w", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
configBytes, err := io.ReadAll(cfgData)
|
// remove comments from qr-code config as it is not needed
|
||||||
if err != nil {
|
sb := strings.Builder{}
|
||||||
|
scanner := bufio.NewScanner(cfgData)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if !strings.HasPrefix(line, "#") {
|
||||||
|
sb.WriteString(line)
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
return nil, fmt.Errorf("failed to read peer config for %s: %w", id, err)
|
return nil, fmt.Errorf("failed to read peer config for %s: %w", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
code, err := qrcode.New(string(configBytes))
|
code, err := qrcode.New(sb.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initializeqr code for %s: %w", id, err)
|
return nil, fmt.Errorf("failed to initializeqr code for %s: %w", id, err)
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ type WireGuardManager interface {
|
|||||||
CreateDefaultPeer(ctx context.Context, user *domain.User) error
|
CreateDefaultPeer(ctx context.Context, user *domain.User) error
|
||||||
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error)
|
GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error)
|
||||||
|
GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error)
|
||||||
GetAllInterfaces(ctx context.Context) ([]domain.Interface, error)
|
GetAllInterfaces(ctx context.Context) ([]domain.Interface, error)
|
||||||
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
PrepareInterface(ctx context.Context) (*domain.Interface, error)
|
PrepareInterface(ctx context.Context) (*domain.Interface, error)
|
||||||
@ -44,6 +45,7 @@ type WireGuardManager interface {
|
|||||||
CreateMultiplePeers(ctx context.Context, id domain.InterfaceIdentifier, r *domain.PeerCreationRequest) ([]domain.Peer, error)
|
CreateMultiplePeers(ctx context.Context, id domain.InterfaceIdentifier, r *domain.PeerCreationRequest) ([]domain.Peer, error)
|
||||||
UpdatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
UpdatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||||
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||||
|
ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatisticsCollector interface {
|
type StatisticsCollector interface {
|
||||||
|
@ -899,3 +899,44 @@ func (m Manager) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier
|
|||||||
|
|
||||||
return m.db.GetPeersStats(ctx, peerIds...)
|
return m.db.GetPeersStats(ctx, peerIds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Manager) GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error) {
|
||||||
|
peers, err := m.db.GetUserPeers(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch peers for user %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peerIds := make([]domain.PeerIdentifier, len(peers))
|
||||||
|
for i, peer := range peers {
|
||||||
|
peerIds[i] = peer.Identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.db.GetPeersStats(ctx, peerIds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Manager) ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error {
|
||||||
|
existingInterface, err := m.db.GetInterface(ctx, in.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to load existing interface %s: %w", in.Identifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.validateInterfaceModifications(ctx, existingInterface, in); err != nil {
|
||||||
|
return fmt.Errorf("update not allowed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, err := m.db.GetInterfacePeers(ctx, in.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find peers for interface %s: %w", in.Identifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range peers {
|
||||||
|
(&peers[i]).ApplyInterfaceDefaults(in)
|
||||||
|
|
||||||
|
_, err := m.UpdatePeer(ctx, &peers[i])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to apply interface defaults to peer %s: %w", peers[i].Identifier, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -92,6 +92,22 @@ func (p *Peer) GetConfigFileName() string {
|
|||||||
return filename
|
return filename
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Peer) ApplyInterfaceDefaults(in *Interface) {
|
||||||
|
p.Endpoint.TrySetValue(in.PeerDefEndpoint)
|
||||||
|
p.EndpointPublicKey.TrySetValue(in.PublicKey)
|
||||||
|
p.AllowedIPsStr.TrySetValue(in.PeerDefAllowedIPsStr)
|
||||||
|
p.PersistentKeepalive.TrySetValue(in.PeerDefPersistentKeepalive)
|
||||||
|
p.Interface.DnsStr.TrySetValue(in.PeerDefDnsStr)
|
||||||
|
p.Interface.DnsSearchStr.TrySetValue(in.PeerDefDnsSearchStr)
|
||||||
|
p.Interface.Mtu.TrySetValue(in.PeerDefMtu)
|
||||||
|
p.Interface.FirewallMark.TrySetValue(in.PeerDefFirewallMark)
|
||||||
|
p.Interface.RoutingTable.TrySetValue(in.PeerDefRoutingTable)
|
||||||
|
p.Interface.PreUp.TrySetValue(in.PeerDefPreUp)
|
||||||
|
p.Interface.PostUp.TrySetValue(in.PeerDefPostUp)
|
||||||
|
p.Interface.PreDown.TrySetValue(in.PeerDefPreDown)
|
||||||
|
p.Interface.PostDown.TrySetValue(in.PeerDefPostDown)
|
||||||
|
}
|
||||||
|
|
||||||
type PeerInterfaceConfig struct {
|
type PeerInterfaceConfig struct {
|
||||||
KeyPair // private/public Key of the peer
|
KeyPair // private/public Key of the peer
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user