mirror of
https://github.com/donaldzou/WGDashboard.git
synced 2024-12-26 17:04:14 +08:00
UI for restore configuration is done
This commit is contained in:
parent
82a472f368
commit
a606626053
6 changed files with 614 additions and 24 deletions
|
@ -462,11 +462,13 @@ class WireguardConfiguration:
|
|||
self.PostDown: str = ""
|
||||
self.SaveConfig: bool = True
|
||||
self.Name = name
|
||||
self.__configPath = os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], f'{self.Name}.conf')
|
||||
|
||||
if name is not None:
|
||||
self.__parseConfigurationFile()
|
||||
else:
|
||||
self.Name = data["ConfigurationName"]
|
||||
self.__configPath = os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], f'{self.Name}.conf')
|
||||
for i in dir(self):
|
||||
if str(i) in data.keys():
|
||||
if isinstance(getattr(self, i), bool):
|
||||
|
@ -484,8 +486,7 @@ class WireguardConfiguration:
|
|||
"SaveConfig": "true"
|
||||
}
|
||||
|
||||
with open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1],
|
||||
f"{self.Name}.conf"), "w+") as configFile:
|
||||
with open(self.__configPath, "w+") as configFile:
|
||||
self.__parser.write(configFile)
|
||||
|
||||
|
||||
|
@ -498,7 +499,7 @@ class WireguardConfiguration:
|
|||
self.getRestrictedPeersList()
|
||||
|
||||
def __parseConfigurationFile(self):
|
||||
self.__parser.read_file(open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], f'{self.Name}.conf')))
|
||||
self.__parser.read_file(open(self.__configPath))
|
||||
sections = self.__parser.sections()
|
||||
if "Interface" not in sections:
|
||||
raise self.InvalidConfigurationFileException(
|
||||
|
@ -510,16 +511,14 @@ class WireguardConfiguration:
|
|||
setattr(self, i, _strToBool(interfaceConfig[i]))
|
||||
else:
|
||||
setattr(self, i, interfaceConfig[i])
|
||||
|
||||
if self.PrivateKey:
|
||||
self.PublicKey = self.__getPublicKey()
|
||||
|
||||
self.Status = self.getStatus()
|
||||
|
||||
def __dropDatabase(self):
|
||||
existingTables = sqlSelect(f"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '{self.Name}%'").fetchall()
|
||||
for t in existingTables:
|
||||
sqlUpdate(f"DROP TABLE {t['name']}")
|
||||
sqlUpdate("DROP TABLE '%s'" % t['name'])
|
||||
|
||||
existingTables = sqlSelect(f"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '{self.Name}%'").fetchall()
|
||||
|
||||
|
@ -950,8 +949,7 @@ class WireguardConfiguration:
|
|||
for l in self.__dumpDatabase():
|
||||
f.write(l + "\n")
|
||||
|
||||
|
||||
def getBackups(self) -> list[dict[str: str, str: str, str: str]]:
|
||||
def getBackups(self, databaseContent: bool = False) -> list[dict[str: str, str: str, str: str]]:
|
||||
backups = []
|
||||
|
||||
directory = os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup')
|
||||
|
@ -963,11 +961,16 @@ class WireguardConfiguration:
|
|||
if _regexMatch(f"^({self.Name})_(.*)\.(conf)$", f):
|
||||
s = re.search(f"^({self.Name})_(.*)\.(conf)$", f)
|
||||
date = s.group(2)
|
||||
backups.append({
|
||||
d = {
|
||||
"filename": f,
|
||||
"backupDate": date,
|
||||
"content": open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup', f), 'r').read()
|
||||
})
|
||||
}
|
||||
if f.replace(".conf", ".sql") in list(os.listdir(directory)):
|
||||
d['database'] = True
|
||||
if databaseContent:
|
||||
d['databaseContent'] = open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup', f.replace(".conf", ".sql")), 'r').read()
|
||||
backups.append(d)
|
||||
|
||||
return backups
|
||||
|
||||
|
@ -1047,7 +1050,15 @@ class WireguardConfiguration:
|
|||
if not status:
|
||||
return False, msg
|
||||
return True, ""
|
||||
|
||||
|
||||
def deleteConfiguration(self):
|
||||
if self.getStatus():
|
||||
self.toggleConfiguration()
|
||||
os.remove(self.__configPath)
|
||||
self.__dropDatabase()
|
||||
return True
|
||||
|
||||
|
||||
class Peer:
|
||||
def __init__(self, tableData, configuration: WireguardConfiguration):
|
||||
self.configuration = configuration
|
||||
|
@ -1545,6 +1556,8 @@ def sqlUpdate(statement: str, paramters: tuple = ()) -> sqlite3.Cursor:
|
|||
with sqldb:
|
||||
cursor = sqldb.cursor()
|
||||
try:
|
||||
statement = statement.rstrip(';')
|
||||
s = f'BEGIN TRANSACTION;{statement};END TRANSACTION;'
|
||||
cursor.execute(statement, paramters)
|
||||
sqldb.commit()
|
||||
except sqlite3.OperationalError as error:
|
||||
|
@ -1748,6 +1761,18 @@ def API_updateWireguardConfiguration():
|
|||
|
||||
return ResponseObject(status, message=msg, data=WireguardConfigurations[name])
|
||||
|
||||
@app.post(f'{APP_PREFIX}/api/deleteWireguardConfiguration')
|
||||
def API_deleteWireguardConfiguration():
|
||||
data = request.get_json()
|
||||
if "Name" not in data.keys() or data.get("Name") is None or data.get("Name") not in WireguardConfigurations.keys():
|
||||
return ResponseObject(False, "Please provide the configuration name you want to delete")
|
||||
|
||||
status = WireguardConfigurations[data.get("Name")].deleteConfiguration()
|
||||
|
||||
if status:
|
||||
WireguardConfigurations.pop(data.get("Name"))
|
||||
return ResponseObject(status)
|
||||
|
||||
@app.get(f'{APP_PREFIX}/api/getWireguardConfigurationBackup')
|
||||
def API_getWireguardConfigurationBackup():
|
||||
configurationName = request.args.get('configurationName')
|
||||
|
@ -1755,6 +1780,43 @@ def API_getWireguardConfigurationBackup():
|
|||
return ResponseObject(False, "Configuration does not exist")
|
||||
return ResponseObject(data=WireguardConfigurations[configurationName].getBackups())
|
||||
|
||||
@app.get(f'{APP_PREFIX}/api/getAllWireguardConfigurationBackup')
|
||||
def API_getAllWireguardConfigurationBackup():
|
||||
data = {
|
||||
"ExistingConfigurations": {},
|
||||
"NonExistingConfigurations": {}
|
||||
}
|
||||
existingConfiguration = WireguardConfigurations.keys()
|
||||
for i in existingConfiguration:
|
||||
b = WireguardConfigurations[i].getBackups(True)
|
||||
if len(b) > 0:
|
||||
data['ExistingConfigurations'][i] = WireguardConfigurations[i].getBackups(True)
|
||||
|
||||
directory = os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup')
|
||||
files = [(file, os.path.getctime(os.path.join(directory, file)))
|
||||
for file in os.listdir(directory) if os.path.isfile(os.path.join(directory, file))]
|
||||
files.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
for f, ct in files:
|
||||
if _regexMatch(f"^(.*)_(.*)\.(conf)$", f):
|
||||
s = re.search(f"^(.*)_(.*)\.(conf)$", f)
|
||||
name = s.group(1)
|
||||
if name not in existingConfiguration:
|
||||
if name not in data['NonExistingConfigurations'].keys():
|
||||
data['NonExistingConfigurations'][name] = []
|
||||
|
||||
date = s.group(2)
|
||||
d = {
|
||||
"filename": f,
|
||||
"backupDate": date,
|
||||
"content": open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup', f), 'r').read()
|
||||
}
|
||||
if f.replace(".conf", ".sql") in list(os.listdir(directory)):
|
||||
d['database'] = True
|
||||
d['databaseContent'] = open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup', f.replace(".conf", ".sql")), 'r').read()
|
||||
data['NonExistingConfigurations'][name].append(d)
|
||||
return ResponseObject(data=data)
|
||||
|
||||
@app.get(f'{APP_PREFIX}/api/createWireguardConfigurationBackup')
|
||||
def API_createWireguardConfigurationBackup():
|
||||
configurationName = request.args.get('configurationName')
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
123
src/static/app/src/views/restoreConfiguration.vue
Normal file
123
src/static/app/src/views/restoreConfiguration.vue
Normal 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>
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue