Feature for #844

This commit is contained in:
Donald Zou
2025-09-08 15:12:16 +08:00
parent 8bbb4a48f7
commit b3889bb1e3
5 changed files with 504 additions and 34 deletions

View File

@@ -0,0 +1,250 @@
<script setup lang="ts">
import LocaleText from "@/components/text/localeText.vue";
import {computed, onMounted, reactive, ref, watch} from "vue";
import {containsCidr, expandCidr, mergeCidr, parseCidr} from "cidr-tools";
import {fetchPost} from "@/utilities/fetch.js"
const props = defineProps(['template', 'edit', 'isNew', 'peersCount'])
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore";
const store = WireguardConfigurationsStore()
const edit = ref(false)
if (props.edit){
edit.value = true
}
const data = ref({...props.template})
const peersCount = ref(256)
const groups = ref([])
const show = ref(20)
const emits = defineEmits(['subnet', 'port', 'update', 'remove'])
const selectedSubnet = ref(undefined)
const selectedPort = ref(undefined)
const ports = ref([])
const availableSubnets = () => {
groups.value = []
if (props.template.Subnet){
let templateExpand = new Set([...expandCidr(props.template.Subnet)])
if (props.peersCount && props.peersCount > 0){
for (let c of store.Configurations){
let address = c.Address.replace(" ", "").split(",")
for (let a of address){
if (containsCidr(props.template.Subnet, a)){
templateExpand = templateExpand.difference(
new Set([...expandCidr(a)])
)
}
}
}
let groupsCount = Math.floor(templateExpand.size / props.peersCount)
let sliced = 0
templateExpand = Array.from(templateExpand)
for (let g = 0; g < (groupsCount > 10 ? 10 : groupsCount); g++){
groups.value.push(mergeCidr(templateExpand.slice(sliced, sliced + props.peersCount)))
sliced += props.peersCount
}
}
}
}
const availablePorts = () => {
if (props.template.ListenPortStart && props.template.ListenPortEnd){
let start = props.template.ListenPortStart
let end = props.template.ListenPortEnd
if (start > end){
start = props.template.ListenPortEnd
end = props.template.ListenPortStart
}
let p = new Set(Array.from(
{
length: end - start + 1
}, (val, index) => start + index
))
ports.value = [...p.difference(new Set(store.Configurations.map(
c => Number(c.ListenPort)
)))]
}
}
onMounted(() => {
if (!props.isNew){
availableSubnets()
availablePorts()
}
})
watch(() => props.peersCount, () => {
availableSubnets()
})
watch(selectedSubnet, () => {
emits("subnet", selectedSubnet.value)
})
watch(selectedPort, () => {
emits("port", selectedPort.value)
})
watch(() => props.template, () => {
availableSubnets()
availablePorts()
}, {
deep: true
})
const readyToSave = computed(() => {
try{
const {start, end} = parseCidr(data.value.Subnet)
if (end - start >= 1000000n) {
throw new Error("Too many IPs");
}
return data.value.Subnet && data.value.ListenPortStart && data.value.ListenPortEnd && (data.value.ListenPortEnd >= data.value.ListenPortStart)
}catch (e){
return false
}
})
const saveTemplate = async () => {
await fetchPost("/api/newConfigurationTemplates/updateTemplate", {
Template: data.value
}, (res) => {
if (res.status){
emits('update', data.value)
edit.value = false
}
})
}
const deleteTemplate = async () => {
await fetchPost("/api/newConfigurationTemplates/deleteTemplate", {
Template: data.value
}, (res) => {
if (res.status){
emits('remove', data)
}
})
}
</script>
<template>
<div class="card rounded-3">
<div class="card-body ">
<div class="row">
<div class="col-sm">
<div class="d-flex flex-column gap-2">
<div class="d-flex align-items-center">
<label class="text-muted">
<small><LocaleText t="Subnet"></LocaleText></small>
</label>
<p class="mb-0 ms-auto" v-if="!edit"><small>{{ template.Subnet }}</small></p>
<input class="form-control-sm form-control rounded-3 w-auto ms-auto" v-model="data.Subnet" v-else>
</div>
<div class="d-flex gap-2 flex-column" v-if="!edit">
<label class="text-muted d-flex align-items-center gap-1" style="white-space: nowrap">
<small><LocaleText t="Available Subnets"></LocaleText></small>
<span class="badge rounded-pill text-bg-success ms-auto">
{{ groups.length }}
</span>
</label>
<select
v-model="selectedSubnet"
class="form-select form-select-sm rounded-3 w-100 ms-auto">
<option :value="undefined" disabled>
<LocaleText t="Select..."></LocaleText>
</option>
<option v-for="s in groups" :value='s.join(", ")'>
{{ s.join(", ") }}
</option>
</select>
</div>
</div>
</div>
<div class="col-sm">
<div class="d-flex flex-column gap-2 h-100">
<div class="d-flex align-items-center">
<label class="text-muted">
<small><LocaleText t="Listen Port Range"></LocaleText></small>
</label>
<p class="mb-0 ms-auto" v-if="!edit">
<small>
{{ template.ListenPortStart }}<i class="bi bi-arrow-right mx-2"></i>
{{ template.ListenPortEnd }}
</small>
</p>
<div v-else class="d-flex ms-auto align-items-center">
<input class="form-control-sm form-control rounded-3 ms-auto"
style="width: 80px"
v-model="data.ListenPortStart"
type="number"
>
<i class="bi bi-arrow-right mx-2"></i>
<input class="form-control-sm form-control rounded-3 ms-auto"
style="width: 80px"
v-model="data.ListenPortEnd"
type="number"
>
</div>
</div>
<div class="d-flex gap-2 flex-column mt-auto" v-if="!edit">
<label class="text-muted d-flex align-items-center gap-1" style="white-space: nowrap">
<small><LocaleText t="Available Ports"></LocaleText></small>
<span class="badge rounded-pill text-bg-success ms-auto">
{{ ports.length }}
</span>
</label>
<select
v-model="selectedPort"
class="form-select form-select-sm rounded-3 w-100 ms-auto">
<option :value="undefined" disabled>
<LocaleText t="Select..."></LocaleText>
</option>
<option v-for="p in [...ports]" :value='p'>
{{ p }}
</option>
</select>
</div>
</div>
</div>
</div>
<hr>
<div class="d-flex gap-2" v-if="!edit">
<button
type="button"
@click="edit = true; data = {...props.template}"
class="ms-auto btn btn-sm border-primary-subtle bg-primary-subtle text-primary-emphasis rounded-3">
<LocaleText t="Edit"></LocaleText>
</button>
<button
type="button"
@click="deleteTemplate()"
class="btn btn-sm border-danger-subtle bg-danger-subtle text-danger-emphasis rounded-3">
<LocaleText t="Delete"></LocaleText>
</button>
</div>
<div class="d-flex gap-2" v-else>
<button
type="button"
@click="isNew ? emits('remove') : edit = false"
class="ms-auto btn btn-sm border-secondary-subtle bg-secondary-subtle text-secondary-emphasis rounded-3">
<LocaleText t="Cancel"></LocaleText>
</button>
<button
type="button"
@click="saveTemplate()"
:class="{disabled: !readyToSave}"
class="btn btn-sm border-primary-subtle bg-primary-subtle text-primary-emphasis rounded-3">
<LocaleText t="Save"></LocaleText>
</button>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,88 @@
<script setup lang="ts">
import LocaleText from "@/components/text/localeText.vue";
import {computed, ref} from "vue";
import {fetchGet} from "@/utilities/fetch.js"
import NewConfigurationTemplate from "@/components/newConfigurationComponents/newConfigurationTemplate.vue";
const emits = defineEmits(['subnet', 'port'])
const templates = ref([])
const getTemplates = async () => {
await fetchGet('/api/newConfigurationTemplates', {}, (res) => {
templates.value = res.data
})
}
await getTemplates()
const newTemplates = ref([])
const newTemplate = async () => {
await fetchGet('/api/newConfigurationTemplates/createTemplate', {}, (res) => {
newTemplates.value.push(res.data)
})
}
const numberOfIP = ref(256)
const calculateIP = ref(256)
</script>
<template>
<div class="card">
<div class="card-header">
<div class="d-flex align-items-center">
<LocaleText t="Templates"></LocaleText>
<button
type="button"
@click="newTemplate()"
class="btn btn-sm bg-success-subtle text-success-emphasis border-success-subtle rounded-3 ms-auto">
<i class="bi bi-plus-circle me-2"></i><LocaleText t="Add Template"></LocaleText>
</button>
</div>
<small class="text-muted">
<LocaleText t="Create templates to keep track a list of available Subnets & Listen Ports"></LocaleText>
</small>
</div>
<div class="card-body">
<div class="d-flex gap-2 align-items-center mb-2" v-if="templates.length > 0">
<label class="text-muted" style="white-space: nowrap">
<small><LocaleText t="No. of IP Address / Subnet"></LocaleText></small>
</label>
<input type="number"
v-model="numberOfIP"
@change="calculateIP = numberOfIP"
class="form-control form-control-sm rounded-3 w-100 ms-auto">
</div>
<div class="row g-2">
<div class="col-12" v-if="newTemplates.length === 0 && templates.length === 0">
<p class="text-center text-muted m-0">
<LocaleText t="No Templates"></LocaleText>
</p>
</div>
<div class="col-12" v-for="template in newTemplates">
<NewConfigurationTemplate
:edit="true"
:isNew="true"
@remove="newTemplates = newTemplates.filter(x => x.TemplateID !== template.TemplateID)"
@update="newTemplates = newTemplates.filter(x => x.TemplateID !== template.TemplateID); getTemplates()"
@subnet="args => emits('subnet', args)"
@port="args => emits('port', args)"
:template="template"></NewConfigurationTemplate>
</div>
<div class="col-12" v-for="(template, index) in templates">
<NewConfigurationTemplate
:key="template.TemplateID"
:peersCount="calculateIP"
@remove="getTemplates()"
@update="getTemplates()"
@subnet="args => emits('subnet', args)"
@port="args => emits('port', args)"
:template="template"></NewConfigurationTemplate>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -1,5 +1,5 @@
<script>
import {parseCidr} from "cidr-tools";
import {parseCidr, containsCidr, mergeCidr, expandCidr} from "cidr-tools";
import '@/utilities/wireguard.js'
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
@@ -7,10 +7,12 @@ import LocaleText from "@/components/text/localeText.vue";
import {parseInterface, parsePeers} from "@/utilities/parseConfigurationFile.js";
import {ref} from "vue";
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
import {exp} from "qrcode/lib/core/galois-field.js";
import NewConfigurationTemplates from "@/components/newConfigurationComponents/newConfigurationTemplates.vue";
export default {
name: "newConfiguration",
components: {LocaleText},
components: {NewConfigurationTemplates, LocaleText},
async setup(){
const store = WireguardConfigurationsStore()
const protocols = ref([])
@@ -137,49 +139,57 @@ export default {
watch: {
'newConfiguration.Address'(newVal){
let ele = document.querySelector("#Address");
ele.classList.remove("is-invalid", "is-valid")
try{
if (newVal.trim().split("/").filter(x => x.length > 0).length !== 2){
throw Error()
if (ele){
ele.classList.remove("is-invalid", "is-valid")
try{
this.numberOfAvailableIPs = 0
newVal.replace(" ", "").split(",").forEach(x => {
let p = parseCidr(x);
let i = Number(p.end - p.start);
this.numberOfAvailableIPs += i + 1;
})
ele.classList.add("is-valid")
}catch (e) {
console.log(e)
this.numberOfAvailableIPs = "0";
ele.classList.add("is-invalid")
}
let p = parseCidr(newVal);
let i = p.end - p.start;
this.numberOfAvailableIPs = i.toLocaleString();
ele.classList.add("is-valid")
}catch (e) {
this.numberOfAvailableIPs = "0";
ele.classList.add("is-invalid")
}
},
'newConfiguration.ListenPort'(newVal){
let ele = document.querySelector("#ListenPort");
ele.classList.remove("is-invalid", "is-valid")
if (newVal < 0 || newVal > 65353 || !Number.isInteger(newVal)){
ele.classList.add("is-invalid")
}else{
ele.classList.add("is-valid")
if (ele){
ele.classList.remove("is-invalid", "is-valid")
if (newVal < 0 || newVal > 65353 || !Number.isInteger(newVal)){
ele.classList.add("is-invalid")
}else{
ele.classList.add("is-valid")
}
}
},
'newConfiguration.ConfigurationName'(newVal){
let ele = document.querySelector("#ConfigurationName");
ele.classList.remove("is-invalid", "is-valid")
if (!/^[a-zA-Z0-9_=+.-]{1,15}$/.test(newVal) || newVal.length === 0 || this.store.Configurations.find(x => x.Name === newVal)){
ele.classList.add("is-invalid")
}else{
ele.classList.add("is-valid")
if (ele){
ele.classList.remove("is-invalid", "is-valid")
if (!/^[a-zA-Z0-9_=+.-]{1,15}$/.test(newVal) || newVal.length === 0 || this.store.Configurations.find(x => x.Name === newVal)){
ele.classList.add("is-invalid")
}else{
ele.classList.add("is-valid")
}
}
},
'newConfiguration.PrivateKey'(newVal){
let ele = document.querySelector("#PrivateKey");
ele.classList.remove("is-invalid", "is-valid")
try{
wireguard.generatePublicKey(newVal)
ele.classList.add("is-valid")
}catch (e) {
ele.classList.add("is-invalid")
if (ele){
ele.classList.remove("is-invalid", "is-valid")
try{
wireguard.generatePublicKey(newVal)
ele.classList.add("is-valid")
}catch (e) {
ele.classList.add("is-invalid")
}
}
}
},
@@ -304,6 +314,10 @@ export default {
</div>
</div>
</div>
<NewConfigurationTemplates
@subnet="args => this.newConfiguration.Address = args"
@port="args => this.newConfiguration.ListenPort = args"
></NewConfigurationTemplates>
<div class="card rounded-3 shadow">
<div class="card-header">
<LocaleText t="Listen Port"></LocaleText>