mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2026-04-21 12:16:17 +00:00
427 lines
14 KiB
Vue
427 lines
14 KiB
Vue
<script>
|
|
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";
|
|
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: {NewConfigurationTemplates, LocaleText},
|
|
async setup(){
|
|
const store = WireguardConfigurationsStore()
|
|
const protocols = ref([])
|
|
await fetchGet("/api/protocolsEnabled", {}, (res) => {
|
|
protocols.value = res.data
|
|
})
|
|
const dashboardStore = DashboardConfigurationStore();
|
|
|
|
return {store, protocols, dashboardStore}
|
|
},
|
|
data(){
|
|
return {
|
|
newConfiguration: {
|
|
ConfigurationName: "",
|
|
Address: "",
|
|
ListenPort: "",
|
|
PrivateKey: "",
|
|
PublicKey: "",
|
|
PresharedKey: "",
|
|
PreUp: "",
|
|
PreDown: "",
|
|
PostUp: "",
|
|
PostDown: "",
|
|
Table: "",
|
|
Protocol: "wg",
|
|
Jc: 5,
|
|
Jmin: 49,
|
|
Jmax: 998,
|
|
S1: 17,
|
|
S2: 110,
|
|
S3: 1,
|
|
S4: 2,
|
|
H1: 0,
|
|
H2: 0,
|
|
H3: 0,
|
|
H4: 0,
|
|
I1: "0",
|
|
I2: "0",
|
|
I3: "0",
|
|
I4: "0",
|
|
I5: "0"
|
|
},
|
|
numberOfAvailableIPs: "0",
|
|
error: false,
|
|
errorMessage: "",
|
|
success: false,
|
|
loading: false,
|
|
parseInterfaceResult: undefined,
|
|
parsePeersResult: undefined
|
|
}
|
|
},
|
|
created() {
|
|
this.wireguardGenerateKeypair();
|
|
|
|
// Generate 4 random numbers for H1, H2, H3, H4
|
|
['H1', 'H2', 'H3', 'H4'].forEach(key => {
|
|
this.newConfiguration[key] = this.rand(1, 2**31);
|
|
});
|
|
|
|
// Initialize I1 to I5 as "0"
|
|
['I1', 'I2', 'I3', 'I4', 'I5'].forEach(key => {
|
|
this.newConfiguration[key] = "0";
|
|
});
|
|
},
|
|
methods: {
|
|
rand(min, max){
|
|
return Math.floor(Math.random() * (max - min) + min);
|
|
},
|
|
wireguardGenerateKeypair(){
|
|
const wg = window.wireguard.generateKeypair();
|
|
this.newConfiguration.PrivateKey = wg.privateKey;
|
|
this.newConfiguration.PublicKey = wg.publicKey;
|
|
this.newConfiguration.PresharedKey = wg.presharedKey;
|
|
},
|
|
async saveNewConfiguration(){
|
|
if (this.goodToSubmit){
|
|
this.loading = true;
|
|
await fetchPost("/api/addWireguardConfiguration", this.newConfiguration, async (res) => {
|
|
if (res.status){
|
|
this.success = true
|
|
await this.store.getConfigurations()
|
|
this.$router.push(`/configuration/${this.newConfiguration.ConfigurationName}/peers`)
|
|
}else{
|
|
this.error = true;
|
|
this.errorMessage = res.message;
|
|
document.querySelector(`#${res.data}`).classList.remove("is-valid")
|
|
document.querySelector(`#${res.data}`).classList.add("is-invalid")
|
|
this.loading = false;
|
|
}
|
|
})
|
|
}
|
|
},
|
|
openFileUpload(){
|
|
document.querySelector("#fileUpload").click();
|
|
},
|
|
readFile(e){
|
|
const file = e.target.files[0];
|
|
if (!file) return false;
|
|
const reader = new FileReader();
|
|
reader.onload = (evt) => {
|
|
this.parseInterfaceResult = parseInterface(evt.target.result);
|
|
this.parsePeersResult = parsePeers(evt.target.result);
|
|
let appliedFields = 0;
|
|
if (this.parseInterfaceResult){
|
|
this.newConfiguration.ConfigurationName = file.name.replace('.conf', '')
|
|
for (let i of Object.keys(this.parseInterfaceResult)){
|
|
if (Object.keys(this.newConfiguration).includes(i)){
|
|
this.newConfiguration[i] = this.parseInterfaceResult[i];
|
|
appliedFields += 1;
|
|
}
|
|
}
|
|
}
|
|
if (appliedFields > 0){
|
|
this.dashboardStore.newMessage("WGDashboard", `Parse successful! Updated ${appliedFields} field(s)`, "success")
|
|
}else {
|
|
this.dashboardStore.newMessage("WGDashboard", `Parse failed`, "danger")
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
},
|
|
computed: {
|
|
goodToSubmit(){
|
|
let requirements = ["ConfigurationName", "Address", "ListenPort", "PrivateKey"]
|
|
let elements = [...document.querySelectorAll("input[required]")];
|
|
return requirements.find(x => {
|
|
return this.newConfiguration[x].length === 0
|
|
}) === undefined && elements.find(x => {
|
|
return x.classList.contains("is-invalid")
|
|
}) === undefined
|
|
}
|
|
},
|
|
watch: {
|
|
'newConfiguration.Address'(newVal){
|
|
let ele = document.querySelector("#Address");
|
|
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")
|
|
}
|
|
}
|
|
},
|
|
'newConfiguration.ListenPort'(newVal){
|
|
let ele = document.querySelector("#ListenPort");
|
|
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");
|
|
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");
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
const fileUpload = document.querySelector("#fileUpload");
|
|
fileUpload.addEventListener("change", this.readFile, false)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="mt-md-5 mt-3 text-body">
|
|
<div class="container mb-4">
|
|
<div class="mb-4 d-flex align-items-center gap-4 align-items-center">
|
|
<RouterLink to="/"
|
|
class="btn btn-dark btn-brand p-2 shadow" style="border-radius: 100%">
|
|
<h2 class="mb-0" style="line-height: 0">
|
|
<i class="bi bi-arrow-left-circle"></i>
|
|
</h2>
|
|
</RouterLink>
|
|
<h2 class="mb-0">
|
|
<LocaleText t="New Configuration"></LocaleText>
|
|
</h2>
|
|
<div class="d-flex gap-2 ms-auto">
|
|
<button class="titleBtn py-2 text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle"
|
|
@click="openFileUpload()"
|
|
type="button" aria-expanded="false">
|
|
<i class="bi bi-upload me-2"></i>
|
|
<LocaleText t="Open File"></LocaleText>
|
|
</button>
|
|
<input type="file" id="fileUpload" multiple class="d-none" accept="text/plain" />
|
|
</div>
|
|
</div>
|
|
|
|
<form class="text-body d-flex flex-column gap-3"
|
|
@submit="(e) => {e.preventDefault(); this.saveNewConfiguration();}"
|
|
>
|
|
<div class="card rounded-3 shadow">
|
|
<div class="card-header">
|
|
<LocaleText t="Protocol"></LocaleText>
|
|
</div>
|
|
<div class="card-body d-flex gap-2 protocolBtnGroup">
|
|
<a
|
|
v-if="this.protocols.includes('wg')"
|
|
@click="this.newConfiguration.Protocol = 'wg'"
|
|
:class="{'opacity-50': this.newConfiguration.Protocol !== 'wg'}"
|
|
class="btn btn-primary wireguardBg border-0 " style="flex-basis: 100%">
|
|
<i class="bi bi-check-circle-fill me-2" v-if="this.newConfiguration.Protocol === 'wg'"></i>
|
|
<i class="bi bi-circle me-2" v-else></i>
|
|
<strong>
|
|
WireGuard
|
|
</strong>
|
|
</a>
|
|
<a
|
|
@click="this.newConfiguration.Protocol = 'awg'"
|
|
v-if="this.protocols.includes('awg')"
|
|
:class="{'opacity-50': this.newConfiguration.Protocol !== 'awg'}"
|
|
class="btn btn-primary amneziawgBg border-0" style="flex-basis: 100%">
|
|
<i class="bi bi-check-circle-fill me-2" v-if="this.newConfiguration.Protocol === 'awg'"></i>
|
|
<i class="bi bi-circle me-2" v-else></i>
|
|
<strong>
|
|
AmneziaWG
|
|
</strong>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card rounded-3 shadow">
|
|
<div class="card-header">
|
|
<LocaleText t="Configuration Name"></LocaleText>
|
|
</div>
|
|
<div class="card-body">
|
|
<input type="text" class="form-control" placeholder="ex. wg1" id="ConfigurationName"
|
|
v-model="this.newConfiguration.ConfigurationName"
|
|
:disabled="this.loading"
|
|
required>
|
|
<div class="invalid-feedback">
|
|
<div v-if="this.error">{{this.errorMessage}}</div>
|
|
<div v-else>
|
|
<LocaleText t="Configuration name is invalid. Possible reasons:"></LocaleText>
|
|
<ul class="mb-0">
|
|
<li>
|
|
<LocaleText t="Configuration name already exist."></LocaleText>
|
|
</li>
|
|
<li>
|
|
<LocaleText t="Configuration name can only contain 15 lower/uppercase alphabet, numbers, underscore, equal sign, plus sign, period and hyphen."></LocaleText>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card rounded-3 shadow">
|
|
<div class="card-header">
|
|
<LocaleText t="Private Key"></LocaleText> & <LocaleText t="Public Key"></LocaleText>
|
|
</div>
|
|
<div class="card-body" style="font-family: var(--bs-font-monospace)">
|
|
<div class="mb-2">
|
|
<label class="text-muted fw-bold mb-1"><small>
|
|
<LocaleText t="Private Key"></LocaleText>
|
|
</small></label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="PrivateKey" required
|
|
:disabled="this.loading"
|
|
v-model="this.newConfiguration.PrivateKey" disabled
|
|
>
|
|
<button class="btn btn-outline-primary" type="button"
|
|
title="Regenerate Private Key"
|
|
@click="wireguardGenerateKeypair()"
|
|
>
|
|
<i class="bi bi-arrow-repeat"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="text-muted fw-bold mb-1"><small>
|
|
<LocaleText t="Public Key"></LocaleText>
|
|
</small></label>
|
|
<input type="text" class="form-control" id="PublicKey"
|
|
v-model="this.newConfiguration.PublicKey" disabled
|
|
>
|
|
</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>
|
|
</div>
|
|
<div class="card-body">
|
|
<input type="number" class="form-control" placeholder="0-65353" id="ListenPort"
|
|
min="1"
|
|
max="65353"
|
|
v-model="this.newConfiguration.ListenPort"
|
|
:disabled="this.loading"
|
|
required>
|
|
<div class="invalid-feedback">
|
|
<div v-if="this.error">{{this.errorMessage}}</div>
|
|
<div v-else>
|
|
<LocaleText t="Invalid port"></LocaleText>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card rounded-3 shadow">
|
|
<div class="card-header d-flex align-items-center">
|
|
<LocaleText t="IP Address/CIDR"></LocaleText>
|
|
<span class="badge rounded-pill text-bg-success ms-auto"><LocaleText :t="numberOfAvailableIPs + ' Available IP Address'"></LocaleText></span>
|
|
</div>
|
|
<div class="card-body">
|
|
<input type="text" class="form-control"
|
|
placeholder="Ex: 10.0.0.1/24" id="Address"
|
|
v-model="this.newConfiguration.Address"
|
|
:disabled="this.loading"
|
|
required>
|
|
<div class="invalid-feedback">
|
|
<div v-if="this.error">{{this.errorMessage}}</div>
|
|
<div v-else>
|
|
IP Address/CIDR is invalid
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
<div class="accordion" id="newConfigurationOptionalAccordion">
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#newConfigurationOptionalAccordionCollapse">
|
|
<LocaleText t="Optional Settings"></LocaleText>
|
|
</button>
|
|
</h2>
|
|
<div id="newConfigurationOptionalAccordionCollapse"
|
|
class="accordion-collapse collapse" data-bs-parent="#newConfigurationOptionalAccordion">
|
|
<div class="accordion-body d-flex flex-column gap-3">
|
|
<div class="card rounded-3" v-for="key in ['Table', 'PreUp', 'PreDown', 'PostUp', 'PostDown']">
|
|
<div class="card-header">{{ key }}</div>
|
|
<div class="card-body">
|
|
<input type="text"
|
|
class="form-control font-monospace" :id="key" v-model="this.newConfiguration[key]">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card rounded-3"
|
|
v-if="this.newConfiguration.Protocol === 'awg'"
|
|
v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5']">
|
|
<div class="card-header">{{ key }}</div>
|
|
<div class="card-body">
|
|
<input type="text"
|
|
class="form-control font-monospace" :id="key" v-model="this.newConfiguration[key]">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto"
|
|
:disabled="!this.goodToSubmit || this.loading || this.success">
|
|
<span v-if="this.success" class="d-flex w-100">
|
|
<LocaleText t="Success"></LocaleText>!
|
|
<i class="bi bi-check-circle-fill ms-2"></i>
|
|
</span>
|
|
<span v-else-if="!this.loading" class="d-flex w-100">
|
|
<i class="bi bi-save-fill me-2"></i>
|
|
<LocaleText t="Save"></LocaleText>
|
|
</span>
|
|
<span v-else class="d-flex w-100 align-items-center">
|
|
<LocaleText t="Saving..."></LocaleText>
|
|
<span class="ms-2 spinner-border spinner-border-sm" role="status">
|
|
</span>
|
|
</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.protocolBtnGroup a{
|
|
transition: all 0.2s ease-in-out;
|
|
}
|
|
</style> |