Mikrotik integration (#467)
Some checks failed
Docker / Build and Push (push) Has been cancelled
github-pages / deploy (push) Has been cancelled
Docker / release (push) Has been cancelled

Allow MikroTik routes as WireGuard backends
This commit is contained in:
h44z
2025-08-10 14:42:02 +02:00
committed by GitHub
parent a86f83a219
commit 112f6bfb77
40 changed files with 3150 additions and 205 deletions

View File

@@ -10,11 +10,13 @@ import isCidr from "is-cidr";
import {isIP} from 'is-ip';
import { freshInterface } from '@/helpers/models';
import {peerStore} from "@/stores/peers";
import {settingsStore} from "@/stores/settings";
const { t } = useI18n()
const interfaces = interfaceStore()
const peers = peerStore()
const settings = settingsStore()
const props = defineProps({
interfaceId: String,
@@ -48,6 +50,26 @@ const currentTags = ref({
PeerDefDnsSearch: ""
})
const formData = ref(freshInterface())
const isSaving = ref(false)
const isDeleting = ref(false)
const isApplyingDefaults = ref(false)
const isBackendValid = computed(() => {
if (!props.visible || !selectedInterface.value) {
return true // if modal is not visible or no interface is selected, we don't care about backend validity
}
let backendId = selectedInterface.value.Backend
let valid = false
let availableBackends = settings.Setting('AvailableBackends') || []
availableBackends.forEach(backend => {
if (backend.Id === backendId) {
valid = true
}
})
return valid
})
// functions
@@ -61,6 +83,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
formData.value.Identifier = interfaces.Prepared.Identifier
formData.value.DisplayName = interfaces.Prepared.DisplayName
formData.value.Mode = interfaces.Prepared.Mode
formData.value.Backend = interfaces.Prepared.Backend
formData.value.PublicKey = interfaces.Prepared.PublicKey
formData.value.PrivateKey = interfaces.Prepared.PrivateKey
@@ -99,6 +122,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
formData.value.Identifier = selectedInterface.value.Identifier
formData.value.DisplayName = selectedInterface.value.DisplayName
formData.value.Mode = selectedInterface.value.Mode
formData.value.Backend = selectedInterface.value.Backend
formData.value.PublicKey = selectedInterface.value.PublicKey
formData.value.PrivateKey = selectedInterface.value.PrivateKey
@@ -237,6 +261,8 @@ function handleChangePeerDefDnsSearch(tags) {
}
async function save() {
if (isSaving.value) return
isSaving.value = true
try {
if (props.interfaceId!=='#NEW#') {
await interfaces.UpdateInterface(selectedInterface.value.Identifier, formData.value)
@@ -251,6 +277,8 @@ async function save() {
text: e.toString(),
type: 'error',
})
} finally {
isSaving.value = false
}
}
@@ -259,6 +287,8 @@ async function applyPeerDefaults() {
return; // do nothing for new interfaces
}
if (isApplyingDefaults.value) return
isApplyingDefaults.value = true
try {
await interfaces.ApplyPeerDefaults(selectedInterface.value.Identifier, formData.value)
@@ -276,10 +306,14 @@ async function applyPeerDefaults() {
text: e.toString(),
type: 'error',
})
} finally {
isApplyingDefaults.value = false
}
}
async function del() {
if (isDeleting.value) return
isDeleting.value = true
try {
await interfaces.DeleteInterface(selectedInterface.value.Identifier)
close()
@@ -290,6 +324,8 @@ async function del() {
text: e.toString(),
type: 'error',
})
} finally {
isDeleting.value = false
}
}
@@ -314,13 +350,22 @@ async function del() {
<label class="form-label mt-4">{{ $t('modals.interface-edit.identifier.label') }}</label>
<input v-model="formData.Identifier" class="form-control" :placeholder="$t('modals.interface-edit.identifier.placeholder')" type="text">
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.mode.label') }}</label>
<select v-model="formData.Mode" class="form-select">
<option value="server">{{ $t('modals.interface-edit.mode.server') }}</option>
<option value="client">{{ $t('modals.interface-edit.mode.client') }}</option>
<option value="any">{{ $t('modals.interface-edit.mode.any') }}</option>
</select>
<div class="row">
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.interface-edit.mode.label') }}</label>
<select v-model="formData.Mode" class="form-select">
<option value="server">{{ $t('modals.interface-edit.mode.server') }}</option>
<option value="client">{{ $t('modals.interface-edit.mode.client') }}</option>
<option value="any">{{ $t('modals.interface-edit.mode.any') }}</option>
</select>
</div>
<div class="form-group col-md-6">
<label class="form-label mt-4" for="ifaceBackendSelector">{{ $t('modals.interface-edit.backend.label') }}</label>
<select id="ifaceBackendSelector" v-model="formData.Backend" class="form-select" aria-describedby="backendHelp">
<option v-for="backend in settings.Setting('AvailableBackends')" :value="backend.Id">{{ backend.Id === 'local' ? $t(backend.Name) : backend.Name }}</option>
</select>
<small v-if="!isBackendValid" id="backendHelp" class="form-text text-warning">{{ $t('modals.interface-edit.backend.invalid-label') }}</small>
</div>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.interface-edit.display-name.label') }}</label>
@@ -385,12 +430,14 @@ async function del() {
<label class="form-label mt-4">{{ $t('modals.interface-edit.mtu.label') }}</label>
<input v-model="formData.Mtu" class="form-control" :placeholder="$t('modals.interface-edit.mtu.placeholder')" type="number">
</div>
<div class="form-group col-md-6">
<div class="form-group col-md-6" v-if="formData.Backend==='local'">
<label class="form-label mt-4">{{ $t('modals.interface-edit.firewall-mark.label') }}</label>
<input v-model="formData.FirewallMark" class="form-control" :placeholder="$t('modals.interface-edit.firewall-mark.placeholder')" type="number">
</div>
<div class="form-group col-md-6" v-else>
</div>
</div>
<div class="row">
<div class="row" v-if="formData.Backend==='local'">
<div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.interface-edit.routing-table.label') }}</label>
<input v-model="formData.RoutingTable" aria-describedby="routingTableHelp" class="form-control" :placeholder="$t('modals.interface-edit.routing-table.placeholder')" type="text">
@@ -530,16 +577,25 @@ async function del() {
</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">{{ $t('modals.interface-edit.button-apply-defaults') }}</button>
<button class="btn btn-primary me-1" type="button" @click.prevent="applyPeerDefaults" :disabled="isApplyingDefaults">
<span v-if="isApplyingDefaults" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('modals.interface-edit.button-apply-defaults') }}
</button>
</fieldset>
</div>
</div>
</template>
<template #footer>
<div class="flex-fill text-start">
<button v-if="props.interfaceId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
<button v-if="props.interfaceId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $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-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.save') }}
</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
</template>
</Modal>

View File

@@ -73,6 +73,8 @@ const currentTags = ref({
DnsSearch: ""
})
const formData = ref(freshPeer())
const isSaving = ref(false)
const isDeleting = ref(false)
// functions
@@ -270,6 +272,8 @@ function handleChangeDnsSearch(tags) {
}
async function save() {
if (isSaving.value) return
isSaving.value = true
try {
if (props.peerId !== '#NEW#') {
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
@@ -278,26 +282,30 @@ async function save() {
}
close()
} catch (e) {
// console.log(e)
notify({
title: "Failed to save peer!",
text: e.toString(),
type: 'error',
})
} finally {
isSaving.value = false
}
}
async function del() {
if (isDeleting.value) return
isDeleting.value = true
try {
await peers.DeletePeer(selectedPeer.value.Identifier)
close()
} catch (e) {
// console.log(e)
notify({
title: "Failed to delete peer!",
text: e.toString(),
type: 'error',
})
} finally {
isDeleting.value = false
}
}
@@ -470,10 +478,15 @@ async function del() {
</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>
<button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $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-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.save') }}
</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
</template>
</Modal>

View File

@@ -38,6 +38,7 @@ function freshForm() {
const currentTag = ref("")
const formData = ref(freshForm())
const isSaving = ref(false)
const title = computed(() => {
if (!props.visible) {
@@ -60,12 +61,15 @@ function handleChangeUserIdentifiers(tags) {
}
async function save() {
if (isSaving.value) return
isSaving.value = true
if (formData.value.Identifiers.length === 0) {
notify({
title: "Missing Identifiers",
text: "At least one identifier is required to create a new peer.",
type: 'error',
})
isSaving.value = false
return
}
@@ -79,6 +83,8 @@ async function save() {
text: e.toString(),
type: 'error',
})
} finally {
isSaving.value = false
}
}
@@ -108,7 +114,10 @@ async function save() {
</fieldset>
</template>
<template #footer>
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.save') }}
</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
</template>
</Modal>

View File

@@ -34,6 +34,8 @@ const title = computed(() => {
})
const formData = ref(freshUser())
const isSaving = ref(false)
const isDeleting = ref(false)
const passwordWeak = computed(() => {
return formData.value.Password && formData.value.Password.length > 0 && formData.value.Password.length < settings.Setting('MinPasswordLength')
@@ -89,6 +91,8 @@ function close() {
}
async function save() {
if (isSaving.value) return
isSaving.value = true
try {
if (props.userId!=='#NEW#') {
await users.UpdateUser(selectedUser.value.Identifier, formData.value)
@@ -102,10 +106,14 @@ async function save() {
text: e.toString(),
type: 'error',
})
} finally {
isSaving.value = false
}
}
async function del() {
if (isDeleting.value) return
isDeleting.value = true
try {
await users.DeleteUser(selectedUser.value.Identifier)
close()
@@ -115,6 +123,8 @@ async function del() {
text: e.toString(),
type: 'error',
})
} finally {
isDeleting.value = false
}
}
@@ -193,9 +203,15 @@ async function del() {
</template>
<template #footer>
<div class="flex-fill text-start">
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.delete') }}
</button>
</div>
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="!formValid">{{ $t('general.save') }}</button>
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="!formValid || isSaving">
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.save') }}
</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
</template>
</Modal>

View File

@@ -55,6 +55,8 @@ const title = computed(() => {
})
const formData = ref(freshPeer())
const isSaving = ref(false)
const isDeleting = ref(false)
// functions
@@ -163,6 +165,8 @@ function close() {
}
async function save() {
if (isSaving.value) return
isSaving.value = true
try {
if (props.peerId !== '#NEW#') {
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
@@ -171,26 +175,30 @@ async function save() {
}
close()
} catch (e) {
// console.log(e)
notify({
title: "Failed to save peer!",
text: e.toString(),
type: 'error',
})
} finally {
isSaving.value = false
}
}
async function del() {
if (isDeleting.value) return
isDeleting.value = true
try {
await peers.DeletePeer(selectedPeer.value.Identifier)
close()
} catch (e) {
// console.log(e)
notify({
title: "Failed to delete peer!",
text: e.toString(),
type: 'error',
})
} finally {
isDeleting.value = false
}
}
@@ -283,10 +291,15 @@ async function del() {
</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>
<button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $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-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('general.save') }}
</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
</template>
</Modal>