mirror of
https://github.com/h44z/wg-portal.git
synced 2025-08-10 07:22:24 +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 {isIP} from 'is-ip';
|
||||
import { freshInterface } from '@/helpers/models';
|
||||
import {peerStore} from "@/stores/peers";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const interfaces = interfaceStore()
|
||||
const peers = peerStore()
|
||||
|
||||
const props = defineProps({
|
||||
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() {
|
||||
try {
|
||||
await interfaces.DeleteInterface(selectedInterface.value.Identifier)
|
||||
@ -312,11 +339,11 @@ async function del() {
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeAddresses"/>
|
||||
</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>
|
||||
<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 class="form-group">
|
||||
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interfaceedit.dns') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Dns"
|
||||
placeholder="DNS Servers"
|
||||
@ -324,7 +351,7 @@ async function del() {
|
||||
:validate="validateIP"
|
||||
@on-tags-changed="handleChangeDns"/>
|
||||
</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>
|
||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch"
|
||||
placeholder="DNS Search prefixes"
|
||||
@ -463,6 +490,10 @@ async function del() {
|
||||
<textarea v-model="formData.PeerDefPostDown" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
@ -132,11 +132,43 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
||||
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.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() {
|
||||
formData.value = freshPeer()
|
||||
emit('close')
|
||||
@ -346,6 +378,10 @@ async function del() {
|
||||
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
|
||||
<label class="form-check-label" >Disabled</label>
|
||||
</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 class="form-group col-md-6">
|
||||
<label class="form-label">{{ $t('modals.peeredit.expiresat') }}</label>
|
||||
|
@ -131,8 +131,9 @@ function email() {
|
||||
<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>
|
||||
<li>Expires At: {{ selectedPeer.ExpiresAt }}</li>
|
||||
<li v-if="selectedPeer.Notes">Notes: {{ selectedPeer.Notes }}</li>
|
||||
<li v-if="selectedPeer.ExpiresAt">Expires At: {{ selectedPeer.ExpiresAt }}</li>
|
||||
<li v-if="selectedPeer.Disabled">Disabled Reason: {{ selectedPeer.DisabledReason }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
|
@ -115,7 +115,10 @@ export function freshPeer() {
|
||||
PostDown: {
|
||||
Value: "",
|
||||
Overridable: true,
|
||||
}
|
||||
},
|
||||
|
||||
// Internal value
|
||||
IgnoreGlobalSettings: false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,9 @@
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"login": "Login",
|
||||
"lang": "Toggle Language"
|
||||
"lang": "Toggle Language",
|
||||
"profile": "My Profile",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"home": {
|
||||
"h1": "WireGuard® VPN Portal",
|
||||
@ -49,13 +51,13 @@
|
||||
"h1": "Interface Administration",
|
||||
"h2": "Current VPN Peers",
|
||||
"h2-client": "Current Endpoints",
|
||||
"tableHeadings": [
|
||||
"Name",
|
||||
"User",
|
||||
"IP's",
|
||||
"Endpoint",
|
||||
"Handshake"
|
||||
],
|
||||
"tableHeadings": {
|
||||
"name": "Name",
|
||||
"user": "User",
|
||||
"ip": "IP's",
|
||||
"endpoint": "Endpoint",
|
||||
"stats": "Status"
|
||||
},
|
||||
"noInterface": {
|
||||
"h1": "No interfaces found...",
|
||||
"message": "Click the plus button above to create a new WireGuard interface."
|
||||
@ -79,10 +81,6 @@
|
||||
"h4": "No peers for the selected interface...",
|
||||
"message": "Click the plus button above to create a new WireGuard interface."
|
||||
},
|
||||
"pagination": {
|
||||
"size": "Number of Elements",
|
||||
"all": "All (slow)"
|
||||
},
|
||||
"peer": {
|
||||
"new": "Create new peer",
|
||||
"edit": "Edit peer"
|
||||
@ -116,9 +114,24 @@
|
||||
"addUser": "Add User",
|
||||
"addMulti": "Add Multiple Users"
|
||||
},
|
||||
"profile": {
|
||||
"h2-clients": "My VPN Peers",
|
||||
"tableHeadings": {
|
||||
"name": "Name",
|
||||
"ip": "IP's",
|
||||
"stats": "Status",
|
||||
"interface": "Server Interface"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"peeredit": {
|
||||
"privatekey": "Private Key"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"pagination": {
|
||||
"size": "Number of Elements",
|
||||
"all": "All (slow)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,4 +32,15 @@ app.use(createPinia());
|
||||
app.use(router);
|
||||
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");
|
||||
|
@ -123,6 +123,30 @@ export const interfaceStore = defineStore({
|
||||
console.log(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)
|
||||
},
|
||||
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,
|
||||
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 {authStore} from "@/stores/auth";
|
||||
import { base64_url_encode } from '@/helpers/encoding';
|
||||
|
||||
import {freshStats} from "@/helpers/models";
|
||||
|
||||
const baseUrl = `/user`
|
||||
|
||||
@ -11,6 +11,8 @@ export const profileStore = defineStore({
|
||||
id: 'profile',
|
||||
state: () => ({
|
||||
peers: [],
|
||||
stats: {},
|
||||
statsEnabled: false,
|
||||
user: {},
|
||||
filter: "",
|
||||
pageSize: 10,
|
||||
@ -40,6 +42,10 @@ export const profileStore = defineStore({
|
||||
hasNextPage: (state) => state.pageOffset < (state.FilteredPeerCount - state.pageSize),
|
||||
hasPrevPage: (state) => state.pageOffset > 0,
|
||||
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: {
|
||||
afterPageSizeChange() {
|
||||
@ -77,6 +83,14 @@ export const profileStore = defineStore({
|
||||
this.user = user
|
||||
this.fetching = false
|
||||
},
|
||||
setStats(statsResponse) {
|
||||
if (!statsResponse) {
|
||||
this.stats = {}
|
||||
this.statsEnabled = false
|
||||
}
|
||||
this.stats = statsResponse.Stats
|
||||
this.statsEnabled = statsResponse.Enabled
|
||||
},
|
||||
async LoadPeers() {
|
||||
this.fetching = true
|
||||
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() {
|
||||
this.fetching = true
|
||||
let currentUser = authStore().user.Identifier
|
||||
|
@ -8,6 +8,7 @@ import InterfaceViewModal from "../components/InterfaceViewModal.vue";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {peerStore} from "../stores/peers";
|
||||
import {interfaceStore} from "../stores/interfaces";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
|
||||
const interfaces = interfaceStore()
|
||||
const peers = peerStore()
|
||||
@ -44,6 +45,25 @@ async function download() {
|
||||
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 () => {
|
||||
await interfaces.LoadInterfaces()
|
||||
await peers.LoadPeers(undefined) // use default interface
|
||||
@ -99,11 +119,12 @@ onMounted(async () => {
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-8">
|
||||
{{ $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 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="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>
|
||||
</div>
|
||||
</div>
|
||||
@ -152,7 +173,7 @@ onMounted(async () => {
|
||||
<td>{{interfaces.GetSelected.Mtu}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.statusBox.intervall') }}:</td>
|
||||
<td>{{ $t('interfaces.statusBox.interval') }}:</td>
|
||||
<td>{{interfaces.GetSelected.PeerDefPersistentKeepalive}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -248,7 +269,7 @@ onMounted(async () => {
|
||||
<td>{{interfaces.GetSelected.Mtu}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.statusBox.intervall') }}:</td>
|
||||
<td>{{ $t('interfaces.statusBox.interval') }}:</td>
|
||||
<td>{{interfaces.GetSelected.PeerDefPersistentKeepalive}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -291,11 +312,12 @@ onMounted(async () => {
|
||||
<th scope="col">
|
||||
<input id="flexCheckDefault" class="form-check-input" title="Select all" type="checkbox" value="">
|
||||
</th><!-- select -->
|
||||
<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 v-if="interfaces.GetSelected.Mode==='client'" scope="col">{{ $t('interfaces.tableHeadings[3]') }}</th>
|
||||
<th v-if="peers.hasStatistics" scope="col">{{ $t('interfaces.tableHeadings[4]') }}</th>
|
||||
<th scope="col"></th><!-- status -->
|
||||
<th scope="col">{{ $t('interfaces.tableHeadings.name') }}</th>
|
||||
<th scope="col">{{ $t('interfaces.tableHeadings.user') }}</th>
|
||||
<th scope="col">{{ $t('interfaces.tableHeadings.ip') }}</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 -->
|
||||
</tr>
|
||||
</thead>
|
||||
@ -304,7 +326,11 @@ onMounted(async () => {
|
||||
<th scope="row">
|
||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
||||
</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>
|
||||
<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 class="col-6">
|
||||
<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">
|
||||
<select v-model.number="peers.pageSize" class="form-select" @click="peers.afterPageSizeChange()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="999999999">{{ $t('interfaces.pagination.all') }}</option>
|
||||
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,6 +12,7 @@ const editPeerId = ref("")
|
||||
onMounted(async () => {
|
||||
await profile.LoadUser()
|
||||
await profile.LoadPeers()
|
||||
await profile.LoadStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -46,11 +47,11 @@ onMounted(async () => {
|
||||
<th scope="col">
|
||||
<input id="flexCheckDefault" class="form-check-input" title="Select all" type="checkbox" value="">
|
||||
</th><!-- select -->
|
||||
<th scope="col">{{ $t('profile.tableHeadings[0]') }}</th>
|
||||
<th scope="col">{{ $t('profile.tableHeadings[1]') }}</th>
|
||||
<th scope="col">{{ $t('profile.tableHeadings[2]') }}</th>
|
||||
<th scope="col">{{ $t('profile.tableHeadings[3]') }}</th>
|
||||
<th scope="col">{{ $t('profile.tableHeadings[4]') }}</th>
|
||||
<th scope="col"></th><!-- status -->
|
||||
<th scope="col">{{ $t('profile.tableHeadings.name') }}</th>
|
||||
<th scope="col">{{ $t('profile.tableHeadings.ip') }}</th>
|
||||
<th v-if="profile.hasStatistics" scope="col">{{ $t('profile.tableHeadings.stats') }}</th>
|
||||
<th scope="col">{{ $t('profile.tableHeadings.interface') }}</th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
@ -59,13 +60,23 @@ onMounted(async () => {
|
||||
<th scope="row">
|
||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
||||
</th>
|
||||
<td>{{peer.DisplayName}}</td>
|
||||
<td>{{peer.Identifier}}</td>
|
||||
<td>{{peer.UserIdentifier}}</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" :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>
|
||||
<span v-for="ip in peer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span>
|
||||
</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">
|
||||
<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>
|
||||
@ -94,14 +105,14 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<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">
|
||||
<select v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="999999999">{{ $t('profile.pagination.all') }}</option>
|
||||
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -116,14 +116,14 @@ function editUser(user) {
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<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">
|
||||
<select v-model.number="users.pageSize" class="form-select" @click="users.afterPageSizeChange()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="999999999">{{ $t('interfaces.pagination.all') }}</option>
|
||||
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||
</select>
|
||||
</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";
|
||||
</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">
|
||||
</head>
|
||||
<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.POST("/new", e.handleCreatePost())
|
||||
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())
|
||||
}
|
||||
@ -295,3 +297,76 @@ func (e interfaceEndpoint) handleDelete() gin.HandlerFunc {
|
||||
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
|
||||
}
|
||||
|
||||
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.POST("/new", e.handleCreatePost())
|
||||
apiGroup.GET("/:id/peers", e.handlePeersGet())
|
||||
apiGroup.GET("/:id/stats", e.handleStatsGet())
|
||||
}
|
||||
|
||||
// handleAllGet returns a gorm handler function.
|
||||
@ -164,6 +165,7 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
// @Summary Get peers for the given user.
|
||||
// @Produce json
|
||||
// @Success 200 {object} []model.Peer
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /user/{id}/peers [get]
|
||||
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.
|
||||
//
|
||||
// @ID users_handleDelete
|
||||
|
@ -1,6 +1,7 @@
|
||||
package configfile
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
@ -8,6 +9,7 @@ import (
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/yeqown/go-qrcode/v2"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
configBytes, err := io.ReadAll(cfgData)
|
||||
if err != nil {
|
||||
// remove comments from qr-code config as it is not needed
|
||||
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)
|
||||
}
|
||||
|
||||
code, err := qrcode.New(string(configBytes))
|
||||
code, err := qrcode.New(sb.String())
|
||||
if err != nil {
|
||||
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
|
||||
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, 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)
|
||||
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, 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)
|
||||
UpdatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
|
||||
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||
ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error
|
||||
}
|
||||
|
||||
type StatisticsCollector interface {
|
||||
|
@ -899,3 +899,44 @@ func (m Manager) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
KeyPair // private/public Key of the peer
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user