UI for restore configuration is done

This commit is contained in:
Donald Zou
2024-10-25 00:19:27 +08:00
parent 82a472f368
commit a606626053
6 changed files with 614 additions and 24 deletions

View File

@@ -0,0 +1,80 @@
<script setup>
import {onMounted, ref} from "vue";
import dayjs from "dayjs";
const props = defineProps({
configurationName: String,
backups: Array,
open: false,
selectedConfigurationBackup: Object
})
const emit = defineEmits(["select"])
const showBackups = ref(props.open)
onMounted(() => {
if (props.selectedConfigurationBackup){
document.querySelector(`#${props.selectedConfigurationBackup.filename.replace('.conf', '')}`).scrollIntoView({
behavior: "smooth"
})
}
})
</script>
<template>
<div class="card rounded-3 shadow-sm">
<a role="button" class="card-body d-flex align-items-center text-decoration-none" @click="showBackups = !showBackups">
<div class="d-flex gap-3 align-items-center">
<h6 class="mb-0">
<samp>
{{configurationName}}
</samp>
</h6>
<small class="text-muted">
{{backups.length}} {{backups.length > 1 ? "Backups": "Backup" }}
</small>
</div>
<h5 class="ms-auto mb-0 dropdownIcon text-muted" :class="{active: showBackups}">
<i class="bi bi-chevron-down"></i>
</h5>
</a>
<div class="card-footer p-3 d-flex flex-column gap-2" v-if="showBackups">
<div class="card rounded-3 shadow-sm animate__animated"
:key="b.filename"
@click="() => {emit('select', b)}"
:id="b.filename.replace('.conf', '')"
role="button" v-for="b in backups">
<div class="card-body d-flex p-3 gap-3 align-items-center">
<small>
<i class="bi bi-file-earmark me-2"></i>
<samp>{{b.filename}}</samp>
</small>
<small>
<i class="bi bi-clock-history me-2"></i>
<samp>{{dayjs(b.backupDate).format("YYYY-MM-DD HH:mm:ss")}}</samp>
</small>
<small >
<i class="bi bi-database me-2"></i>
{{b.database? "Yes" : "No" }}
</small>
<small class="text-muted ms-auto">
<i class="bi bi-chevron-right"></i>
</small>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.dropdownIcon{
transition: all 0.2s ease-in-out;
}
.dropdownIcon.active{
transform: rotate(180deg);
}
</style>

View File

@@ -0,0 +1,318 @@
<script setup>
import {computed, onMounted, reactive, ref, watch} from "vue";
import LocaleText from "@/components/text/localeText.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {parse} from "cidr-tools";
const props = defineProps({
selectedConfigurationBackup: Object
})
const newConfiguration = reactive({
ConfigurationName: props.selectedConfigurationBackup.filename.split("_")[0]
})
const lineSplit = props.selectedConfigurationBackup.content.split("\n");
for(let line of lineSplit){
if( line === "[Peer]") break
if (line.length > 0){
let l = line.replace(" = ", "=").split("=")
if (l[0] === "ListenPort"){
newConfiguration[l[0]] = parseInt(l[1])
}else{
newConfiguration[l[0]] = l[1]
}
}
}
const error = ref(false)
const loading = ref(false)
const errorMessage = ref("")
const store = WireguardConfigurationsStore()
const wireguardGenerateKeypair = () => {
const wg = window.wireguard.generateKeypair();
newConfiguration.PrivateKey = wg.privateKey;
newConfiguration.PublicKey = wg.publicKey;
newConfiguration.PresharedKey = wg.presharedKey;
}
const validateConfigurationName = computed(() => {
return /^[a-zA-Z0-9_=+.-]{1,15}$/.test(newConfiguration.ConfigurationName)
&& newConfiguration.ConfigurationName.length > 0
&& !store.Configurations.find(x => x.Name === newConfiguration.ConfigurationName)
})
const validatePrivateKey = computed(() => {
try{
wireguard.generatePublicKey(newConfiguration.PrivateKey)
}catch (e) {
return false
}
return true
})
const validateListenPort = computed(() => {
return newConfiguration.ListenPort > 0
&& newConfiguration.ListenPort <= 65353
&& Number.isInteger(newConfiguration.ListenPort)
&& !store.Configurations.find(x => parseInt(x.ListenPort) === newConfiguration.ListenPort)
})
const validateAddress = computed(() => {
try{
parse(newConfiguration.Address)
return true
}catch (e){
return false
}
})
const validateForm = computed(() => {
return validateAddress.value
&& validateListenPort.value
&& validatePrivateKey.value
&& validateConfigurationName.value
})
onMounted(() => {
document.querySelector("main").scrollTo({
top: 0,
behavior: "smooth"
})
watch(() => validatePrivateKey, (newVal) => {
if (newVal){
newConfiguration.PublicKey = wireguard.generatePublicKey(newConfiguration.PrivateKey)
}
}, {
immediate: true
})
})
const availableIPAddress = computed(() => {
let p;
try{
p = parse(newConfiguration.Address);
}catch (e){
return 0;
}
return p.end - p.start
})
const peersCount = computed(() => {
if (props.selectedConfigurationBackup.database){
let l = props.selectedConfigurationBackup.databaseContent.split("\n")
return l.filter(x => x.search('INSERT INTO "(.*)"') >= 0).length
}
return 0
})
const restrictedPeersCount = computed(() => {
if (props.selectedConfigurationBackup.database){
let l = props.selectedConfigurationBackup.databaseContent.split("\n")
return l.filter(x => x.search('INSERT INTO "(.*)_restrict_access"') >= 0).length
}
return 0
})
</script>
<template>
<div class="d-flex flex-column gap-5" id="confirmBackup">
<form class="d-flex flex-column gap-3">
<div class="d-flex flex-column flex-sm-row align-items-start align-items-sm-center gap-3">
<h4 class="mb-0">
<LocaleText t="Configuration File"></LocaleText>
</h4>
</div>
<div>
<label class="text-muted mb-1" for="ConfigurationName"><small>
<LocaleText t="Configuration Name"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" placeholder="ex. wg1" id="ConfigurationName"
v-model="newConfiguration.ConfigurationName"
:class="[validateConfigurationName ? 'is-valid':'is-invalid']"
:disabled="loading"
required>
<div class="invalid-feedback">
<div v-if="error">{{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 class="row g-3">
<div class="col-sm">
<div>
<label class="text-muted mb-1" for="PrivateKey"><small>
<LocaleText t="Private Key"></LocaleText>
</small></label>
<div class="input-group">
<input type="text" class="form-control rounded-start-3" id="PrivateKey" required
:disabled="loading"
:class="[validatePrivateKey ? 'is-valid':'is-invalid']"
v-model="newConfiguration.PrivateKey" disabled
>
<button class="btn btn-outline-primary rounded-end-3" type="button"
title="Regenerate Private Key"
@click="wireguardGenerateKeypair()"
>
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
</div>
<div class="col-sm">
<div>
<label class="text-muted mb-1" for="PublicKey"><small>
<LocaleText t="Public Key"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" id="PublicKey"
v-model="newConfiguration.PublicKey" disabled
>
</div>
</div>
</div>
<div>
<label class="text-muted mb-1" for="ListenPort"><small>
<LocaleText t="Listen Port"></LocaleText>
</small></label>
<input type="number" class="form-control rounded-3" placeholder="0-65353" id="ListenPort"
min="1"
max="65353"
v-model="newConfiguration.ListenPort"
:class="[validateListenPort ? 'is-valid':'is-invalid']"
:disabled="loading"
required>
<div class="invalid-feedback">
<div v-if="error">{{errorMessage}}</div>
<div v-else>
<LocaleText t="Listen Port is invalid. Possible reasons:"></LocaleText>
<ul class="mb-0">
<li>
<LocaleText t="Invalid port."></LocaleText>
</li>
<li>
<LocaleText t="Port is assigned to existing WireGuard Configuration. "></LocaleText>
</li>
</ul>
</div>
</div>
</div>
<div>
<label class="text-muted mb-1 d-flex" for="ListenPort">
<small>
<LocaleText t="IP Address/CIDR"></LocaleText>
</small>
<small class="ms-auto" :class="[availableIPAddress > 0 ? 'text-success':'text-danger']">
{{availableIPAddress}} Available IP Address
</small>
</label>
<input type="text" class="form-control"
placeholder="Ex: 10.0.0.1/24" id="Address"
v-model="newConfiguration.Address"
:class="[validateAddress ? 'is-valid':'is-invalid']"
:disabled="loading"
required>
<div class="invalid-feedback">
<div v-if="error">{{errorMessage}}</div>
<div v-else>
<LocaleText t="IP Address/CIDR is invalid"></LocaleText>
</div>
</div>
</div>
<div class="accordion" id="newConfigurationOptionalAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed rounded-3"
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>
<label class="text-muted mb-1" for="PreUp"><small>
<LocaleText t="PreUp"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" id="PreUp" v-model="newConfiguration.PreUp">
</div>
<div>
<label class="text-muted mb-1" for="PreDown"><small>
<LocaleText t="PreDown"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" id="PreDown" v-model="newConfiguration.PreDown">
</div>
<div>
<label class="text-muted mb-1" for="PostUp"><small>
<LocaleText t="PostUp"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" id="PostUp" v-model="newConfiguration.PostUp">
</div>
<div>
<label class="text-muted mb-1" for="PostDown"><small>
<LocaleText t="PostDown"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" id="PostDown" v-model="newConfiguration.PostDown">
</div>
</div>
</div>
</div>
</div>
</form>
<div class="d-flex flex-column gap-3">
<div class="d-flex flex-column flex-sm-row align-items-start align-items-sm-center gap-3">
<h4 class="mb-0">
<LocaleText t="Database File"></LocaleText>
</h4>
<h4 class="mb-0 ms-auto" :class="[selectedConfigurationBackup.database ? 'text-success':'text-danger']">
<i class="bi" :class="[selectedConfigurationBackup.database ? 'bi-check-circle-fill':'bi-x-circle-fill']"></i>
</h4>
</div>
<div v-if="selectedConfigurationBackup.database">
<div class="row g-3">
<div class="col-sm">
<div class="card text-bg-success rounded-3">
<div class="card-body">
<i class="bi bi-person-fill me-2"></i> Contain <strong>{{peersCount}}</strong> Peer{{peersCount > 1 ? 's':''}}
</div>
</div>
</div>
<div class="col-sm">
<div class="card text-bg-warning rounded-3">
<div class="card-body">
<i class="bi bi-person-fill-lock me-2"></i> Contain <strong>{{restrictedPeersCount}}</strong> Restricted Peer{{restrictedPeersCount > 1 ? 's':''}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex">
<button class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto"
:disabled="!validateForm"
>
<i class="bi bi-clock-history me-2"></i> Restore
</button>
</div>
</div>
</template>
<style scoped>
</style>

View File

@@ -126,14 +126,23 @@ export default {
</script>
<template>
<div class="mt-5">
<div class="mt-5 text-body">
<div class="container mb-4">
<div class="mb-4 d-flex align-items-center gap-4">
<RouterLink to="/" class="text-decoration-none">
<h3 class="mb-0 text-body">
<i class="bi bi-chevron-left me-4"></i>
<LocaleText t="New Configuration"></LocaleText>
</h3>
<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>
<RouterLink to="/restore_configuration"
class="btn btn-dark btn-brand p-2 shadow ms-auto" style="border-radius: 100%">
<h2 class="mb-0" style="line-height: 0">
<i class="bi bi-clock-history"></i>
</h2>
</RouterLink>
</div>
@@ -168,8 +177,7 @@ export default {
</div>
<div class="card rounded-3 shadow">
<div class="card-header">
<LocaleText t="Private Key"></LocaleText> &
<LocaleText t="Public Key"></LocaleText>
<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">
@@ -286,13 +294,12 @@ export default {
<i class="bi bi-check-circle-fill ms-2"></i>
</span>
<span v-else-if="!this.loading" class="d-flex w-100">
<LocaleText t="Save Configuration"></LocaleText>
<i class="bi bi-save-fill ms-2"></i>
<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 class="visually-hidden">Loading...</span>-->
</span>
</span>

View File

@@ -0,0 +1,123 @@
<script setup>
import LocaleText from "@/components/text/localeText.vue";
import {onMounted, reactive, ref, watch} from "vue";
import {fetchGet} from "@/utilities/fetch.js";
import BackupGroup from "@/components/restoreConfigurationComponents/backupGroup.vue";
import ConfirmBackup from "@/components/restoreConfigurationComponents/confirmBackup.vue";
const backups = ref(undefined)
onMounted(() => {
fetchGet("/api/getAllWireguardConfigurationBackup", {}, (res) => {
backups.value = res.data
})
})
const confirm = ref(false)
const selectedConfigurationBackup = ref(undefined)
const selectedConfiguration = ref("")
</script>
<template>
<div class="mt-5 text-body">
<div class="container mb-4">
<div class="mb-5 d-flex align-items-center gap-4">
<RouterLink to="/new_configuration"
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="Restore Configuration"></LocaleText>
</h2>
</div>
<div name="restore" v-if="backups" >
<div class="d-flex mb-5 align-items-center steps" role="button"
:class="{active: !confirm}"
@click="confirm = false" key="step1">
<div class=" d-flex text-decoration-none text-body flex-grow-1 align-items-center gap-3"
>
<h1 class="mb-0"
style="line-height: 0">
<i class="bi bi-1-circle-fill"></i>
</h1>
<div>
<h4 class="mb-0">Step 1</h4>
<small class="text-muted">
<LocaleText t="Select a backup you want to restore" v-if="!confirm"></LocaleText>
<LocaleText t="Click to change a backup" v-else></LocaleText>
</small>
</div>
</div>
<Transition name="zoomReversed">
<div class="ms-sm-auto" v-if="confirm">
<small class="text-muted">Selected Backup</small>
<h6>
<samp>{{selectedConfigurationBackup.filename}}</samp>
</h6>
</div>
</Transition>
</div>
<div id="step1Detail" v-if="!confirm">
<div class="mb-4">
<h5>Backup of existing WireGuard Configurations</h5>
<hr>
<div class="d-flex gap-3 flex-column">
<BackupGroup
@select="(b) => {selectedConfigurationBackup = b; selectedConfiguration = c; confirm = true}"
:open="selectedConfiguration === c"
:selectedConfigurationBackup="selectedConfigurationBackup"
v-for="c in Object.keys(backups.ExistingConfigurations)"
:configuration-name="c" :backups="backups.ExistingConfigurations[c]"></BackupGroup>
</div>
</div>
<div class="mb-4">
<h5>Backup of non-existing WireGuard Configurations</h5>
<hr>
<div class="d-flex gap-3 flex-column">
<BackupGroup
@select="(b) => {selectedConfigurationBackup = b; selectedConfiguration = c; confirm = true}"
:selectedConfigurationBackup="selectedConfigurationBackup"
:open="selectedConfiguration === c"
v-for="c in Object.keys(backups.NonExistingConfigurations)"
:configuration-name="c" :backups="backups.NonExistingConfigurations[c]"></BackupGroup>
</div>
</div>
</div>
<div class="my-5" key="step2" id="step2">
<div class="steps d-flex text-decoration-none text-body flex-grow-1 align-items-center gap-3"
:class="{active: confirm}"
>
<h1 class="mb-0"
style="line-height: 0">
<i class="bi bi-2-circle-fill"></i>
</h1>
<div>
<h4 class="mb-0">Step 2</h4>
<small class="text-muted">
<LocaleText t="Backup not selected" v-if="!confirm"></LocaleText>
<LocaleText t="Confirm & edit restore information" v-else></LocaleText>
</small>
</div>
</div>
</div>
<ConfirmBackup :selectedConfigurationBackup="selectedConfigurationBackup" v-if="confirm" key="confirm"></ConfirmBackup>
</div>
</div>
</div>
</template>
<style scoped>
.steps{
transition: all 0.3s ease-in-out;
opacity: 0.3;
&.active{
opacity: 1;
}
}
</style>

View File

@@ -38,9 +38,9 @@ export default {
<template>
<div class="mt-md-5 mt-3">
<div class="container-md">
<h3 class="mb-3 text-body">
<h2 class="mb-4 text-body">
<LocaleText t="Settings"></LocaleText>
</h3>
</h2>
<div class="card mb-4 shadow rounded-3">
<p class="card-header">