mirror of
https://github.com/h44z/wg-portal
synced 2025-02-26 05:49:14 +00:00
fix self provisioning feature (#272)
This commit is contained in:
parent
1b8cdc3417
commit
d01d865b4d
294
frontend/src/components/UserPeerEditModal.vue
Normal file
294
frontend/src/components/UserPeerEditModal.vue
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
<script setup>
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
import { peerStore } from "@/stores/peers";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
import { freshPeer, freshInterface } from '@/helpers/models';
|
||||||
|
import { profileStore } from "@/stores/profile";
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const peers = peerStore()
|
||||||
|
const profile = profileStore()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
peerId: String,
|
||||||
|
visible: Boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const selectedPeer = computed(() => {
|
||||||
|
let p = peers.Find(props.peerId)
|
||||||
|
|
||||||
|
if (!p) {
|
||||||
|
if (!!props.peerId || props.peerId.length) {
|
||||||
|
p = profile.peers.find((p) => p.Identifier === props.peerId)
|
||||||
|
} else {
|
||||||
|
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedInterface = computed(() => {
|
||||||
|
let iId = profile.selectedInterfaceId;
|
||||||
|
|
||||||
|
let i = freshInterface() // dummy interface to avoid 'undefined' exceptions
|
||||||
|
if (iId) {
|
||||||
|
i = profile.interfaces.find((i) => i.Identifier === iId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return i
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
if (!props.visible) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedPeer.value) {
|
||||||
|
return t("modals.peer-edit.headline-edit-peer") + " " + selectedPeer.value.Identifier
|
||||||
|
}
|
||||||
|
return t("modals.peer-edit.headline-new-peer")
|
||||||
|
})
|
||||||
|
|
||||||
|
const formData = ref(freshPeer())
|
||||||
|
|
||||||
|
// functions
|
||||||
|
|
||||||
|
watch(() => props.visible, async (newValue, oldValue) => {
|
||||||
|
if (oldValue === false && newValue === true) { // if modal is shown
|
||||||
|
if (!selectedPeer.value) {
|
||||||
|
await peers.PreparePeer(selectedInterface.value.Identifier)
|
||||||
|
|
||||||
|
formData.value.Identifier = peers.Prepared.Identifier
|
||||||
|
formData.value.DisplayName = peers.Prepared.DisplayName
|
||||||
|
formData.value.UserIdentifier = peers.Prepared.UserIdentifier
|
||||||
|
formData.value.InterfaceIdentifier = peers.Prepared.InterfaceIdentifier
|
||||||
|
formData.value.Disabled = peers.Prepared.Disabled
|
||||||
|
formData.value.ExpiresAt = peers.Prepared.ExpiresAt
|
||||||
|
formData.value.Notes = peers.Prepared.Notes
|
||||||
|
|
||||||
|
formData.value.Endpoint = peers.Prepared.Endpoint
|
||||||
|
formData.value.EndpointPublicKey = peers.Prepared.EndpointPublicKey
|
||||||
|
formData.value.AllowedIPs = peers.Prepared.AllowedIPs
|
||||||
|
formData.value.ExtraAllowedIPs = peers.Prepared.ExtraAllowedIPs
|
||||||
|
formData.value.PresharedKey = peers.Prepared.PresharedKey
|
||||||
|
formData.value.PersistentKeepalive = peers.Prepared.PersistentKeepalive
|
||||||
|
|
||||||
|
formData.value.PrivateKey = peers.Prepared.PrivateKey
|
||||||
|
formData.value.PublicKey = peers.Prepared.PublicKey
|
||||||
|
|
||||||
|
formData.value.Mode = peers.Prepared.Mode
|
||||||
|
|
||||||
|
formData.value.Addresses = peers.Prepared.Addresses
|
||||||
|
formData.value.CheckAliveAddress = peers.Prepared.CheckAliveAddress
|
||||||
|
formData.value.Dns = peers.Prepared.Dns
|
||||||
|
formData.value.DnsSearch = peers.Prepared.DnsSearch
|
||||||
|
formData.value.Mtu = peers.Prepared.Mtu
|
||||||
|
formData.value.FirewallMark = peers.Prepared.FirewallMark
|
||||||
|
formData.value.RoutingTable = peers.Prepared.RoutingTable
|
||||||
|
|
||||||
|
formData.value.PreUp = peers.Prepared.PreUp
|
||||||
|
formData.value.PostUp = peers.Prepared.PostUp
|
||||||
|
formData.value.PreDown = peers.Prepared.PreDown
|
||||||
|
formData.value.PostDown = peers.Prepared.PostDown
|
||||||
|
|
||||||
|
} else { // fill existing data
|
||||||
|
formData.value.Identifier = selectedPeer.value.Identifier
|
||||||
|
formData.value.DisplayName = selectedPeer.value.DisplayName
|
||||||
|
formData.value.UserIdentifier = selectedPeer.value.UserIdentifier
|
||||||
|
formData.value.InterfaceIdentifier = selectedPeer.value.InterfaceIdentifier
|
||||||
|
formData.value.Disabled = selectedPeer.value.Disabled
|
||||||
|
formData.value.ExpiresAt = selectedPeer.value.ExpiresAt
|
||||||
|
formData.value.Notes = selectedPeer.value.Notes
|
||||||
|
|
||||||
|
formData.value.Endpoint = selectedPeer.value.Endpoint
|
||||||
|
formData.value.EndpointPublicKey = selectedPeer.value.EndpointPublicKey
|
||||||
|
formData.value.AllowedIPs = selectedPeer.value.AllowedIPs
|
||||||
|
formData.value.ExtraAllowedIPs = selectedPeer.value.ExtraAllowedIPs
|
||||||
|
formData.value.PresharedKey = selectedPeer.value.PresharedKey
|
||||||
|
formData.value.PersistentKeepalive = selectedPeer.value.PersistentKeepalive
|
||||||
|
|
||||||
|
formData.value.PrivateKey = selectedPeer.value.PrivateKey
|
||||||
|
formData.value.PublicKey = selectedPeer.value.PublicKey
|
||||||
|
|
||||||
|
formData.value.Mode = selectedPeer.value.Mode
|
||||||
|
|
||||||
|
formData.value.Addresses = selectedPeer.value.Addresses
|
||||||
|
formData.value.CheckAliveAddress = selectedPeer.value.CheckAliveAddress
|
||||||
|
formData.value.Dns = selectedPeer.value.Dns
|
||||||
|
formData.value.DnsSearch = selectedPeer.value.DnsSearch
|
||||||
|
formData.value.Mtu = selectedPeer.value.Mtu
|
||||||
|
formData.value.FirewallMark = selectedPeer.value.FirewallMark
|
||||||
|
formData.value.RoutingTable = selectedPeer.value.RoutingTable
|
||||||
|
|
||||||
|
formData.value.PreUp = selectedPeer.value.PreUp
|
||||||
|
formData.value.PostUp = selectedPeer.value.PostUp
|
||||||
|
formData.value.PreDown = selectedPeer.value.PreDown
|
||||||
|
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.Disabled, async (newValue, oldValue) => {
|
||||||
|
if (oldValue && !newValue && formData.value.ExpiresAt) {
|
||||||
|
formData.value.ExpiresAt = "" // reset expiry date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
formData.value = freshPeer()
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
try {
|
||||||
|
if (props.peerId !== '#NEW#') {
|
||||||
|
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
|
||||||
|
} else {
|
||||||
|
await peers.CreatePeer(selectedInterface.value.Identifier, formData.value)
|
||||||
|
}
|
||||||
|
close()
|
||||||
|
} catch (e) {
|
||||||
|
// console.log(e)
|
||||||
|
notify({
|
||||||
|
title: "Failed to save peer!",
|
||||||
|
text: e.toString(),
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function del() {
|
||||||
|
try {
|
||||||
|
await peers.DeletePeer(selectedPeer.value.Identifier)
|
||||||
|
close()
|
||||||
|
} catch (e) {
|
||||||
|
// console.log(e)
|
||||||
|
notify({
|
||||||
|
title: "Failed to delete peer!",
|
||||||
|
text: e.toString(),
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="title" :visible="visible" @close="close">
|
||||||
|
<template #default>
|
||||||
|
<fieldset>
|
||||||
|
<legend class="mt-4">{{ $t('modals.peer-edit.header-general') }}</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.display-name.label') }}</label>
|
||||||
|
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.display-name.placeholder')"
|
||||||
|
v-model="formData.DisplayName">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
|
||||||
|
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
|
||||||
|
v-model="formData.PrivateKey">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
|
||||||
|
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
|
||||||
|
v-model="formData.PublicKey">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
|
||||||
|
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
|
||||||
|
v-model="formData.PresharedKey">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend class="mt-4">{{ $t('modals.peer-edit.header-network') }}</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.keep-alive.label') }}</label>
|
||||||
|
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.keep-alive.label')"
|
||||||
|
v-model="formData.PersistentKeepalive.Value">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.mtu.label') }}</label>
|
||||||
|
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.mtu.label')"
|
||||||
|
v-model="formData.Mtu.Value">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend class="mt-4">{{ $t('modals.peer-edit.header-hooks') }}</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-up.label') }}</label>
|
||||||
|
<textarea v-model="formData.PreUp.Value" class="form-control" rows="2"
|
||||||
|
:placeholder="$t('modals.peer-edit.pre-up.placeholder')"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-up.label') }}</label>
|
||||||
|
<textarea v-model="formData.PostUp.Value" class="form-control" rows="2"
|
||||||
|
:placeholder="$t('modals.peer-edit.post-up.placeholder')"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-down.label') }}</label>
|
||||||
|
<textarea v-model="formData.PreDown.Value" class="form-control" rows="2"
|
||||||
|
:placeholder="$t('modals.peer-edit.pre-down.placeholder')"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-down.label') }}</label>
|
||||||
|
<textarea v-model="formData.PostDown.Value" class="form-control" rows="2"
|
||||||
|
:placeholder="$t('modals.peer-edit.post-down.placeholder')"></textarea>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend class="mt-4">{{ $t('modals.peer-edit.header-state') }}</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
|
||||||
|
<label class="form-check-label">{{ $t('modals.peer-edit.disabled.label') }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label class="form-label">{{ $t('modals.peer-edit.expires-at.label') }}</label>
|
||||||
|
<input type="date" pattern="\d{4}-\d{2}-\d{2}" class="form-control" min="2023-01-01"
|
||||||
|
v-model="formData.ExpiresAt">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex-fill text-start">
|
||||||
|
<button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{
|
||||||
|
$t('general.delete') }}</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||||
|
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style></style>
|
@ -12,6 +12,8 @@ export const profileStore = defineStore({
|
|||||||
id: 'profile',
|
id: 'profile',
|
||||||
state: () => ({
|
state: () => ({
|
||||||
peers: [],
|
peers: [],
|
||||||
|
interfaces: [],
|
||||||
|
selectedInterfaceId: "",
|
||||||
stats: {},
|
stats: {},
|
||||||
statsEnabled: false,
|
statsEnabled: false,
|
||||||
user: {},
|
user: {},
|
||||||
@ -71,6 +73,7 @@ export const profileStore = defineStore({
|
|||||||
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
||||||
},
|
},
|
||||||
hasStatistics: (state) => state.statsEnabled,
|
hasStatistics: (state) => state.statsEnabled,
|
||||||
|
CountInterfaces: (state) => state.interfaces.length,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
afterPageSizeChange() {
|
afterPageSizeChange() {
|
||||||
@ -116,6 +119,11 @@ export const profileStore = defineStore({
|
|||||||
this.stats = statsResponse.Stats
|
this.stats = statsResponse.Stats
|
||||||
this.statsEnabled = statsResponse.Enabled
|
this.statsEnabled = statsResponse.Enabled
|
||||||
},
|
},
|
||||||
|
setInterfaces(interfaces) {
|
||||||
|
this.interfaces = interfaces
|
||||||
|
this.selectedInterfaceId = interfaces.length > 0 ? interfaces[0].Identifier : ""
|
||||||
|
this.fetching = false
|
||||||
|
},
|
||||||
async enableApi() {
|
async enableApi() {
|
||||||
this.fetching = true
|
this.fetching = true
|
||||||
let currentUser = authStore().user.Identifier
|
let currentUser = authStore().user.Identifier
|
||||||
@ -186,5 +194,19 @@ export const profileStore = defineStore({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async LoadInterfaces() {
|
||||||
|
this.fetching = true
|
||||||
|
let currentUser = authStore().user.Identifier
|
||||||
|
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}/interfaces`)
|
||||||
|
.then(this.setInterfaces)
|
||||||
|
.catch(error => {
|
||||||
|
this.setInterfaces([])
|
||||||
|
console.log("Failed to load interfaces for ", currentUser, ": ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to load interfaces!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -3,7 +3,7 @@ import PeerViewModal from "../components/PeerViewModal.vue";
|
|||||||
|
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { profileStore } from "@/stores/profile";
|
import { profileStore } from "@/stores/profile";
|
||||||
import PeerEditModal from "@/components/PeerEditModal.vue";
|
import UserPeerEditModal from "@/components/UserPeerEditModal.vue";
|
||||||
import { settingsStore } from "@/stores/settings";
|
import { settingsStore } from "@/stores/settings";
|
||||||
import { humanFileSize } from "@/helpers/utils";
|
import { humanFileSize } from "@/helpers/utils";
|
||||||
|
|
||||||
@ -27,10 +27,18 @@ function sortBy(key) {
|
|||||||
profile.sortOrder = sortOrder.value;
|
profile.sortOrder = sortOrder.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function friendlyInterfaceName(id, name) {
|
||||||
|
if (name) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await profile.LoadUser()
|
await profile.LoadUser()
|
||||||
await profile.LoadPeers()
|
await profile.LoadPeers()
|
||||||
await profile.LoadStats()
|
await profile.LoadStats()
|
||||||
|
await profile.LoadInterfaces()
|
||||||
await profile.calculatePages(); // Forces to show initial page number
|
await profile.calculatePages(); // Forces to show initial page number
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -38,7 +46,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId !== ''" @close="viewedPeerId = ''"></PeerViewModal>
|
<PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId !== ''" @close="viewedPeerId = ''"></PeerViewModal>
|
||||||
<PeerEditModal :peerId="editPeerId" :visible="editPeerId !== ''" @close="editPeerId = ''"></PeerEditModal>
|
<UserPeerEditModal :peerId="editPeerId" :visible="editPeerId !== ''" @close="editPeerId = ''; profile.LoadPeers()"></UserPeerEditModal>
|
||||||
|
|
||||||
<!-- Peer list -->
|
<!-- Peer list -->
|
||||||
<div class="mt-4 row">
|
<div class="mt-4 row">
|
||||||
@ -56,9 +64,17 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-3 text-lg-end">
|
<div class="col-12 col-lg-3 text-lg-end">
|
||||||
<a v-if="settings.Setting('SelfProvisioning')" class="btn btn-primary ms-2" href="#"
|
<div class="form-group" v-if="settings.Setting('SelfProvisioning')">
|
||||||
:title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId = '#NEW#'"><i
|
<div class="input-group mb-3">
|
||||||
class="fa fa-plus me-1"></i><i class="fa fa-user"></i></a>
|
<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">
|
||||||
|
<option v-if="profile.CountInterfaces===0" value="nothing">{{ $t('interfaces.no-interface.default-selection') }}</option>
|
||||||
|
<option v-for="iface in profile.interfaces" :key="iface.Identifier" :value="iface.Identifier">{{ friendlyInterfaceName(iface.Identifier,iface.DisplayName) }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 table-responsive">
|
<div class="mt-2 table-responsive">
|
||||||
|
@ -24,8 +24,8 @@ func (e peerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
|
|||||||
|
|
||||||
apiGroup.GET("/iface/:iface/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
apiGroup.GET("/iface/:iface/all", e.authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
||||||
apiGroup.GET("/iface/:iface/stats", e.authenticator.LoggedIn(ScopeAdmin), e.handleStatsGet())
|
apiGroup.GET("/iface/:iface/stats", e.authenticator.LoggedIn(ScopeAdmin), e.handleStatsGet())
|
||||||
apiGroup.GET("/iface/:iface/prepare", e.authenticator.LoggedIn(ScopeAdmin), e.handlePrepareGet())
|
apiGroup.GET("/iface/:iface/prepare", e.authenticator.LoggedIn(), e.handlePrepareGet())
|
||||||
apiGroup.POST("/iface/:iface/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
apiGroup.POST("/iface/:iface/new", e.authenticator.LoggedIn(), e.handleCreatePost())
|
||||||
apiGroup.POST("/iface/:iface/multiplenew", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreateMultiplePost())
|
apiGroup.POST("/iface/:iface/multiplenew", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreateMultiplePost())
|
||||||
apiGroup.GET("/config-qr/:id", e.handleQrCodeGet())
|
apiGroup.GET("/config-qr/:id", e.handleQrCodeGet())
|
||||||
apiGroup.POST("/config-mail", e.handleEmailPost())
|
apiGroup.POST("/config-mail", e.handleEmailPost())
|
||||||
|
@ -28,6 +28,7 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
|
|||||||
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||||
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
|
apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
|
||||||
apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
|
apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
|
||||||
|
apiGroup.GET("/:id/interfaces", e.authenticator.UserIdMatch("id"), e.handleInterfacesGet())
|
||||||
apiGroup.POST("/:id/api/enable", e.authenticator.UserIdMatch("id"), e.handleApiEnablePost())
|
apiGroup.POST("/:id/api/enable", e.authenticator.UserIdMatch("id"), e.handleApiEnablePost())
|
||||||
apiGroup.POST("/:id/api/disable", e.authenticator.UserIdMatch("id"), e.handleApiDisablePost())
|
apiGroup.POST("/:id/api/disable", e.authenticator.UserIdMatch("id"), e.handleApiDisablePost())
|
||||||
}
|
}
|
||||||
@ -170,6 +171,7 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
|
|||||||
// @ID users_handlePeersGet
|
// @ID users_handlePeersGet
|
||||||
// @Tags Users
|
// @Tags Users
|
||||||
// @Summary Get peers for the given user.
|
// @Summary Get peers for the given user.
|
||||||
|
// @Param id path string true "The user identifier"
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} []model.Peer
|
// @Success 200 {object} []model.Peer
|
||||||
// @Failure 400 {object} model.Error
|
// @Failure 400 {object} model.Error
|
||||||
@ -179,14 +181,14 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
ctx := domain.SetUserInfoFromGin(c)
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
interfaceId := Base64UrlDecode(c.Param("id"))
|
userId := Base64UrlDecode(c.Param("id"))
|
||||||
if interfaceId == "" {
|
if userId == "" {
|
||||||
c.JSON(http.StatusBadRequest,
|
c.JSON(http.StatusBadRequest,
|
||||||
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(interfaceId))
|
peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(userId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError,
|
c.JSON(http.StatusInternalServerError,
|
||||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
@ -202,6 +204,7 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
|
|||||||
// @ID users_handleStatsGet
|
// @ID users_handleStatsGet
|
||||||
// @Tags Users
|
// @Tags Users
|
||||||
// @Summary Get peer stats for the given user.
|
// @Summary Get peer stats for the given user.
|
||||||
|
// @Param id path string true "The user identifier"
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} model.PeerStats
|
// @Success 200 {object} model.PeerStats
|
||||||
// @Failure 400 {object} model.Error
|
// @Failure 400 {object} model.Error
|
||||||
@ -229,6 +232,39 @@ func (e userEndpoint) handleStatsGet() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleInterfacesGet returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleInterfacesGet
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Get interfaces for the given user. Returns an empty list if self provisioning is disabled.
|
||||||
|
// @Param id path string true "The user identifier"
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} []model.Interface
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/{id}/interfaces [get]
|
||||||
|
func (e userEndpoint) handleInterfacesGet() 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
|
||||||
|
}
|
||||||
|
|
||||||
|
peers, err := e.app.GetUserInterfaces(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.NewInterfaces(peers, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// handleDelete returns a gorm handler function.
|
// handleDelete returns a gorm handler function.
|
||||||
//
|
//
|
||||||
// @ID users_handleDelete
|
// @ID users_handleDelete
|
||||||
|
@ -109,7 +109,11 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
|
|||||||
func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface {
|
func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface {
|
||||||
results := make([]Interface, len(src))
|
results := make([]Interface, len(src))
|
||||||
for i := range src {
|
for i := range src {
|
||||||
results[i] = *NewInterface(&src[i], srcPeers[i])
|
if srcPeers == nil {
|
||||||
|
results[i] = *NewInterface(&src[i], nil)
|
||||||
|
} else {
|
||||||
|
results[i] = *NewInterface(&src[i], srcPeers[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
@ -39,6 +39,7 @@ type WireGuardManager interface {
|
|||||||
GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error)
|
GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error)
|
||||||
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||||
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
|
GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error)
|
||||||
PrepareInterface(ctx context.Context) (*domain.Interface, error)
|
PrepareInterface(ctx context.Context) (*domain.Interface, error)
|
||||||
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
|
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
|
||||||
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||||
|
@ -68,6 +68,34 @@ func (m Manager) GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interfa
|
|||||||
return interfaces, allPeers, nil
|
return interfaces, allPeers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserInterfaces returns all interfaces that are available for users to create new peers.
|
||||||
|
// If self-provisioning is disabled, this function will return an empty list.
|
||||||
|
func (m Manager) GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error) {
|
||||||
|
if !m.cfg.Core.SelfProvisioningAllowed {
|
||||||
|
return nil, nil // self-provisioning is disabled - no interfaces for users
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces, err := m.db.GetAllInterfaces(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to load all interfaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip sensitive data, users only need very limited information
|
||||||
|
userInterfaces := make([]domain.Interface, 0, len(interfaces))
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
if iface.IsDisabled() {
|
||||||
|
continue // skip disabled interfaces
|
||||||
|
}
|
||||||
|
if iface.Type != domain.InterfaceTypeServer {
|
||||||
|
continue // skip client interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
userInterfaces = append(userInterfaces, iface.PublicInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInterfaces, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) {
|
func (m Manager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) (int, error) {
|
||||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
@ -62,8 +62,10 @@ func (m Manager) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error) {
|
func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error) {
|
||||||
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
if !m.cfg.Core.SelfProvisioningAllowed {
|
||||||
return nil, err // TODO: self provisioning?
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
@ -73,6 +75,10 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
|
|||||||
return nil, fmt.Errorf("unable to find interface %s: %w", id, err)
|
return nil, fmt.Errorf("unable to find interface %s: %w", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.cfg.Core.SelfProvisioningAllowed && iface.Type != domain.InterfaceTypeServer {
|
||||||
|
return nil, fmt.Errorf("self provisioning is only allowed for server interfaces: %w", domain.ErrNoPermission)
|
||||||
|
}
|
||||||
|
|
||||||
ips, err := m.getFreshPeerIpConfig(ctx, iface)
|
ips, err := m.getFreshPeerIpConfig(ctx, iface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to get fresh ip addresses: %w", err)
|
return nil, fmt.Errorf("unable to get fresh ip addresses: %w", err)
|
||||||
@ -149,10 +155,18 @@ func (m Manager) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
|
func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
|
||||||
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
if !m.cfg.Core.SelfProvisioningAllowed {
|
||||||
return nil, err
|
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
|
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
|
||||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||||
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
|
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
|
||||||
@ -161,6 +175,18 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
|
|||||||
return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
|
return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if a peer is self provisioned, ensure that only allowed fields are set from the request
|
||||||
|
if !sessionUser.IsAdmin {
|
||||||
|
preparedPeer, err := m.PreparePeer(ctx, peer.InterfaceIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to prepare peer for interface %s: %w", peer.InterfaceIdentifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
preparedPeer.OverwriteUserEditableFields(peer)
|
||||||
|
|
||||||
|
peer = preparedPeer
|
||||||
|
}
|
||||||
|
|
||||||
if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil {
|
if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil {
|
||||||
return nil, fmt.Errorf("creation not allowed: %w", err)
|
return nil, fmt.Errorf("creation not allowed: %w", err)
|
||||||
}
|
}
|
||||||
@ -229,6 +255,19 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
|
|||||||
return nil, fmt.Errorf("update not allowed: %w", err)
|
return nil, fmt.Errorf("update not allowed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
|
// if a peer is self provisioned, ensure that only allowed fields are set from the request
|
||||||
|
if !sessionUser.IsAdmin {
|
||||||
|
originalPeer, err := m.db.GetPeer(ctx, peer.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
|
||||||
|
}
|
||||||
|
originalPeer.OverwriteUserEditableFields(peer)
|
||||||
|
|
||||||
|
peer = originalPeer
|
||||||
|
}
|
||||||
|
|
||||||
// handle peer identifier change (new public key)
|
// handle peer identifier change (new public key)
|
||||||
if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
|
if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
|
||||||
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier
|
peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier
|
||||||
@ -438,7 +477,7 @@ func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interfa
|
|||||||
func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain.Peer) error {
|
func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain.Peer) error {
|
||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
|
||||||
return domain.ErrNoPermission
|
return domain.ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,7 +491,7 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
|
|||||||
return fmt.Errorf("invalid peer identifier: %w", domain.ErrInvalidData)
|
return fmt.Errorf("invalid peer identifier: %w", domain.ErrInvalidData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
|
||||||
return domain.ErrNoPermission
|
return domain.ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,7 +506,7 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
|
|||||||
func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) error {
|
func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) error {
|
||||||
currentUser := domain.GetUserInfo(ctx)
|
currentUser := domain.GetUserInfo(ctx)
|
||||||
|
|
||||||
if !currentUser.IsAdmin {
|
if !currentUser.IsAdmin && !m.cfg.Core.SelfProvisioningAllowed {
|
||||||
return domain.ErrNoPermission
|
return domain.ErrNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +72,17 @@ type Interface struct {
|
|||||||
PeerDefPostDown string // default action that is executed after the device is down
|
PeerDefPostDown string // default action that is executed after the device is down
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublicInfo returns a copy of the interface with only the public information.
|
||||||
|
// Sensible information like keys are not included.
|
||||||
|
func (i *Interface) PublicInfo() Interface {
|
||||||
|
return Interface{
|
||||||
|
Identifier: i.Identifier,
|
||||||
|
DisplayName: i.DisplayName,
|
||||||
|
Type: i.Type,
|
||||||
|
Disabled: i.Disabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate performs checks to ensure that the interface is valid.
|
// Validate performs checks to ensure that the interface is valid.
|
||||||
func (i *Interface) Validate() error {
|
func (i *Interface) Validate() error {
|
||||||
// validate peer default endpoint, add port if needed
|
// validate peer default endpoint, add port if needed
|
||||||
|
@ -127,6 +127,19 @@ func (p *Peer) GenerateDisplayName(prefix string) {
|
|||||||
p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8))
|
p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OverwriteUserEditableFields overwrites the user editable fields of the peer with the values from the userPeer
|
||||||
|
func (p *Peer) OverwriteUserEditableFields(userPeer *Peer) {
|
||||||
|
p.DisplayName = userPeer.DisplayName
|
||||||
|
p.Interface.PublicKey = userPeer.Interface.PublicKey
|
||||||
|
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
|
||||||
|
p.Interface.Mtu = userPeer.Interface.Mtu
|
||||||
|
p.PersistentKeepalive = userPeer.PersistentKeepalive
|
||||||
|
p.ExpiresAt = userPeer.ExpiresAt
|
||||||
|
p.Disabled = userPeer.Disabled
|
||||||
|
p.DisabledReason = userPeer.DisabledReason
|
||||||
|
p.PresharedKey = userPeer.PresharedKey
|
||||||
|
}
|
||||||
|
|
||||||
type PeerInterfaceConfig struct {
|
type PeerInterfaceConfig struct {
|
||||||
KeyPair // private/public Key of the peer
|
KeyPair // private/public Key of the peer
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user