chore: add in the WGDashboard assets
@@ -1,4 +1,7 @@
|
||||
|
||||
*.db
|
||||
dist
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
|
||||
@@ -13,7 +13,7 @@ debug_enabled = True
|
||||
wg_conf_path = /etc/wireguard
|
||||
awg_conf_path = /etc/amnezia/amneziawg
|
||||
app_prefix =
|
||||
auth_req = True
|
||||
auth_req = False
|
||||
version = v5.0.0
|
||||
dashboard_refresh_interval = 60000
|
||||
dashboard_peer_list_display = grid
|
||||
|
||||
@@ -39,7 +39,11 @@ if __name__ == '__main__':
|
||||
prefix = config_server.get('app_prefix', '')
|
||||
|
||||
# Configure the Flask app
|
||||
app = flask.Flask("WGDashboard", template_folder=os.path.abspath("./static/dist/WGDashboardAdmin"))
|
||||
app = flask.Flask(
|
||||
"WGDashboard",
|
||||
template_folder=os.path.abspath("./static/dist/WGDashboardAdmin"),
|
||||
static_folder=os.path.abspath("./static/dist/WGDashboardAdmin")
|
||||
)
|
||||
app.register_blueprint(routes, url_prefix=prefix)
|
||||
|
||||
app.wgd_config = config_contents
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import logging as log
|
||||
|
||||
import flask
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from .response import make_resp_obj
|
||||
from .utilities import helpers
|
||||
@@ -69,7 +69,9 @@ def api_authenticate():
|
||||
auth_required_flag = config_server.get('auth_req', True)
|
||||
|
||||
if not auth_required_flag:
|
||||
_, config_other = util.filter_config(flask.current_app.wgd_config, 'OTHER')
|
||||
ok, config_other = util.filter_config(flask.current_app.wgd_config, 'OTHER')
|
||||
if not ok:
|
||||
return make_resp_obj("Internal error", {}, 500)
|
||||
|
||||
return make_resp_obj(
|
||||
"Login successful, no authentication required",
|
||||
@@ -77,19 +79,23 @@ def api_authenticate():
|
||||
200
|
||||
)
|
||||
|
||||
data = request.get_json()
|
||||
data = flask.request.get_json()
|
||||
if not data:
|
||||
return make_resp_obj("Invalid request body", {}, 400)
|
||||
|
||||
return make_resp_obj("Authentication required", {}, 401)
|
||||
|
||||
@routes.route('/', methods=["GET"])
|
||||
def index():
|
||||
return make_resp_obj(
|
||||
"Pong from the /",
|
||||
{},
|
||||
200
|
||||
)
|
||||
@routes.route("/", defaults={"path": ""})
|
||||
@routes.route("/<path:path>")
|
||||
def catch_all(path):
|
||||
static_folder = flask.current_app.static_folder
|
||||
template_folder = flask.current_app.template_folder
|
||||
|
||||
file_path = os.path.join(static_folder, path)
|
||||
if os.path.isfile(file_path):
|
||||
return flask.send_from_directory(static_folder, path)
|
||||
|
||||
return flask.send_from_directory(template_folder, "index.html")
|
||||
|
||||
@routes.route('/health', methods=["GET"])
|
||||
@routes.route('/healthz', methods=["GET"])
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
.vite/*
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
echo "Running vite build..."
|
||||
if vite build; then
|
||||
echo "Vite build successful."
|
||||
else
|
||||
echo "Vite build failed. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
echo "Checking for changes to commit..."
|
||||
if git diff-index --quiet HEAD --; then
|
||||
|
||||
if git commit -a; then
|
||||
echo "Git commit successful."
|
||||
else
|
||||
echo "Git commit failed. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No changes to commit. Skipping commit."
|
||||
fi
|
||||
echo "Pushing changes to remote..."
|
||||
if git push; then
|
||||
echo "Git push successful."
|
||||
else
|
||||
echo "Git push failed. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="WGDashboard">
|
||||
<meta name="apple-mobile-web-app-title" content="WGDashboard">
|
||||
<link rel="manifest" href="./json/manifest.json">
|
||||
<link rel="icon" href="./img/Logo-2-512x512.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WGDashboard</title>
|
||||
<base href="./">
|
||||
<script>
|
||||
const isViteDevMode = document.querySelector('script[src*="@vite/client"]') !== null;
|
||||
const base = document.querySelector('base');
|
||||
if (base && isViteDevMode) {
|
||||
base.href = '/';
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "4.3.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"module": "es2022",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build --emptyOutDir",
|
||||
"buildcommitpush": "./build.sh",
|
||||
"build electron": "vite build --emptyOutDir && vite build --mode electron && cd ../../../../WGDashboard-Desktop && /opt/homebrew/bin/npm run \"electron dist\"",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@volar/language-server": "2.4.28",
|
||||
"@vue/language-server": "3.2.4",
|
||||
"@vuepic/vue-datepicker": "^12.1.0",
|
||||
"@vueuse/core": "^14.2.0",
|
||||
"@vueuse/shared": "^14.1.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"cidr-tools": "^11.0.8",
|
||||
"css-color-converter": "^2.0.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"electron-builder": "^26.7.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"i": "^0.3.7",
|
||||
"is-cidr": "^6.0.3",
|
||||
"npm": "^11.8.0",
|
||||
"ol": "^10.7.0",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"qrcodejs": "^1.0.0",
|
||||
"simple-code-editor": "^2.0.9",
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^3.5.28",
|
||||
"vue-chartjs": "^5.3.3",
|
||||
"vue-router": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"overrides": {
|
||||
"tar": "^7.5.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const proxy = "http://wg.local:10086/"
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 126 KiB |
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"theme_color": "#343a40",
|
||||
"background_color": "#343a40",
|
||||
"display": "fullscreen",
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
"name": "WGDashboard",
|
||||
"short_name": "WGDashboard",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "https://wgdashboard-resources.tor1.cdn.digitaloceanspaces.com/Documentation%20Images/sign-in.png",
|
||||
"sizes": "2880x1826",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide"
|
||||
},
|
||||
{
|
||||
"src": "https://wgdashboard-resources.tor1.cdn.digitaloceanspaces.com/Documentation%20Images/index.png",
|
||||
"sizes": "2880x1826",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"icons": [
|
||||
{
|
||||
"src": "../img/Logo-2-Rounded-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "../img/Logo-2-Rounded-256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "../img/Logo-2-Rounded-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "../img/Logo-2-Rounded-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup async>
|
||||
import {RouterView, useRoute} from 'vue-router'
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {computed, watch} from "vue";
|
||||
const store = DashboardConfigurationStore();
|
||||
import "@/utilities/wireguard.js"
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
store.initCrossServerConfiguration();
|
||||
if (window.IS_WGDASHBOARD_DESKTOP){
|
||||
store.IsElectronApp = true;
|
||||
store.CrossServerConfiguration.Enable = true;
|
||||
if (store.ActiveServerConfiguration){
|
||||
fetchGet("/api/locale", {}, (res) => {
|
||||
store.Locale = res.data
|
||||
})
|
||||
}
|
||||
}else{
|
||||
fetchGet("/api/locale", {}, (res) => {
|
||||
store.Locale = res.data
|
||||
})
|
||||
}
|
||||
watch(store.CrossServerConfiguration, () => {
|
||||
store.syncCrossServerConfiguration()
|
||||
}, {
|
||||
deep: true
|
||||
});
|
||||
const route = useRoute()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-100 bg-body" :data-bs-theme="store.Configuration?.Server.dashboard_theme">
|
||||
<div style="z-index: 9999; height: 5px" class="position-absolute loadingBar top-0 start-0"></div>
|
||||
<nav class="navbar bg-dark sticky-top" data-bs-theme="dark" v-if="!route.meta.hideTopNav">
|
||||
<div class="container-fluid d-flex text-body align-items-center">
|
||||
<RouterLink to="/" class="navbar-brand mb-0 h1">
|
||||
<img src="/img/Logo-2-Rounded-512x512.png" alt="WGDashboard Logo" style="width: 32px">
|
||||
</RouterLink>
|
||||
<a role="button" class="navbarBtn text-body"
|
||||
@click="store.ShowNavBar = !store.ShowNavBar"
|
||||
style="line-height: 0; font-size: 2rem">
|
||||
<Transition name="fade2" mode="out-in">
|
||||
<i class="bi bi-list" v-if="!store.ShowNavBar"></i>
|
||||
<i class="bi bi-x-lg" v-else></i>
|
||||
</Transition>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<Suspense>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition name="app" mode="out-in" type="transition" appear>
|
||||
<Component :is="Component"></Component>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</Suspense>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-enter-active,
|
||||
.app-leave-active {
|
||||
transition: all 0.7s cubic-bezier(0.82, 0.58, 0.17, 1);
|
||||
}
|
||||
.app-enter-from,
|
||||
.app-leave-to{
|
||||
opacity: 0;
|
||||
transform: scale(1.05);
|
||||
filter: blur(8px);
|
||||
}
|
||||
@media screen and (min-width: 768px) {
|
||||
.navbar{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from "vue";
|
||||
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
const props = defineProps(['configuration', 'peers', 'clientAssignedPeers', 'availablePeerSearchString'])
|
||||
const emits = defineEmits(['assign', 'unassign'])
|
||||
const assignmentStore = DashboardClientAssignmentStore()
|
||||
const available = computed(() => {
|
||||
if (props.clientAssignedPeers){
|
||||
if (Object.keys(props.clientAssignedPeers).includes(props.configuration)){
|
||||
return props.peers.filter(
|
||||
x => {
|
||||
return !props.clientAssignedPeers[props.configuration].map(
|
||||
x => x.id
|
||||
).includes(x.id) &&
|
||||
(!props.availablePeerSearchString ||
|
||||
(props.availablePeerSearchString &&
|
||||
(x.id.includes(props.availablePeerSearchString) || x.name.includes(props.availablePeerSearchString))))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
return props.availablePeerSearchString ? props.peers.filter(
|
||||
x => x.id.includes(props.availablePeerSearchString) || x.name.includes(props.availablePeerSearchString)
|
||||
) : props.peers
|
||||
})
|
||||
const confirmDelete = ref(false)
|
||||
const collapse = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card rounded-0 border-0">
|
||||
<div
|
||||
@click="collapse = !collapse"
|
||||
role="button"
|
||||
class="card-header rounded-0 sticky-top bg-body-secondary border-0 border-bottom text-white d-flex">
|
||||
<small><samp>{{ configuration }}</samp></small>
|
||||
<a role="button" class="ms-auto text-white" >
|
||||
<i class="bi bi-chevron-compact-down" v-if="collapse"></i>
|
||||
<i class="bi bi-chevron-compact-up" v-else></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0" v-if="!collapse">
|
||||
<div class="list-group list-group-flush" >
|
||||
<div
|
||||
class="list-group-item d-flex border-bottom list-group-item-action d-flex align-items-center gap-3"
|
||||
:key="peer.id"
|
||||
v-for="peer in available" >
|
||||
<div v-if="!confirmDelete">
|
||||
<small class="text-body">
|
||||
<RouterLink
|
||||
class="text-decoration-none"
|
||||
target="_blank"
|
||||
:to="'/configuration/' + configuration +'/peers?id=' + encodeURIComponent(peer.id)">
|
||||
<samp>{{ peer.id }}</samp>
|
||||
</RouterLink>
|
||||
</small><br>
|
||||
<small class="text-muted">
|
||||
{{ peer.name ? peer.name : 'Untitled Peer'}}
|
||||
</small>
|
||||
</div>
|
||||
<div v-else>
|
||||
<small class="text-body">
|
||||
<LocaleText t="Are you sure to remove this peer?"></LocaleText>
|
||||
</small><br>
|
||||
<small class="text-muted">
|
||||
<samp>{{ peer.id }}</samp>
|
||||
</small>
|
||||
</div>
|
||||
<template v-if="clientAssignedPeers">
|
||||
<button
|
||||
@click="emits('assign', peer.id)"
|
||||
:class="{disabled: assignmentStore.assigning}"
|
||||
class="btn bg-success-subtle text-success-emphasis ms-auto">
|
||||
<i class="bi bi-plus-circle-fill" ></i>
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
v-else
|
||||
@click="emits('unassign', peer.assignment_id)"
|
||||
:class="{disabled: assignmentStore.unassigning}"
|
||||
aria-label="Delete Assignment"
|
||||
class="btn bg-danger-subtle text-danger-emphasis ms-auto">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts" async>
|
||||
import {onMounted, ref, watch, watchEffect} from "vue";
|
||||
import { fetchGet } from "@/utilities/fetch.js"
|
||||
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
|
||||
import AvailablePeersGroup from "@/components/clientComponents/availablePeersGroup.vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
const props = defineProps(['client', 'clientAssignedPeers'])
|
||||
const loading = ref(false)
|
||||
const assignmentStore = DashboardClientAssignmentStore()
|
||||
const manage = ref(false)
|
||||
const emits = defineEmits(['refresh'])
|
||||
|
||||
const assign = async (ConfigurationName, Peer, ClientID) => {
|
||||
await assignmentStore.assignClient(ConfigurationName, Peer, ClientID, false)
|
||||
emits('refresh')
|
||||
}
|
||||
|
||||
const unassign = async (AssignmentID) => {
|
||||
await assignmentStore.unassignClient(undefined, undefined, AssignmentID)
|
||||
emits('refresh')
|
||||
}
|
||||
|
||||
const availablePeerSearchString = ref("")
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-flex rounded-0 border-0 flex-column d-flex flex-column border-bottom pb-1" v-if="!loading">
|
||||
<div class="d-flex flex-column p-3 gap-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<LocaleText t="Assigned Peers"></LocaleText>
|
||||
<span class="text-bg-primary badge ms-2">
|
||||
{{ Object.keys(clientAssignedPeers).length }} <LocaleText :t="Object.keys(clientAssignedPeers).length > 1 ? 'Configurations' : 'Configuration'"></LocaleText>
|
||||
</span>
|
||||
<span class="text-bg-info badge ms-2">
|
||||
{{ Object.values(clientAssignedPeers).flat().length }} <LocaleText :t="Object.values(clientAssignedPeers).flat().length > 1 ? 'Peers' : 'Peer'"></LocaleText>
|
||||
</span>
|
||||
</h6>
|
||||
<button class="btn btn-sm bg-primary-subtle text-primary-emphasis rounded-3 ms-auto"
|
||||
@click="manage = !manage">
|
||||
<template v-if="!manage">
|
||||
<i class="bi bi-list-check me-2"></i>
|
||||
<LocaleText t="Manage"></LocaleText>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="bi bi-check me-2"></i>
|
||||
<LocaleText t="Done"></LocaleText>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
<div class="rounded-3 availablePeers border h-100 overflow-scroll flex-grow-1 d-flex flex-column">
|
||||
<AvailablePeersGroup
|
||||
:configuration="configuration"
|
||||
:peers="peers"
|
||||
@unassign="async (id) => await unassign(id)"
|
||||
v-for="(peers, configuration) in clientAssignedPeers">
|
||||
</AvailablePeersGroup>
|
||||
<h6 class="text-muted m-auto p-3" v-if="Object.keys(clientAssignedPeers).length === 0">
|
||||
<LocaleText t="No peer assigned to this client"></LocaleText>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: 500px" class="d-flex flex-column p-3" v-if="manage">
|
||||
<div class="availablePeers border h-100 card rounded-3">
|
||||
<div class="card-header sticky-top p-3">
|
||||
<h6 class="mb-0 d-flex align-items-center">
|
||||
<LocaleText t="Available Peers"></LocaleText>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-0 overflow-scroll">
|
||||
<AvailablePeersGroup
|
||||
:availablePeerSearchString="availablePeerSearchString"
|
||||
:configuration="configuration"
|
||||
:clientAssignedPeers="clientAssignedPeers"
|
||||
:peers="peers"
|
||||
:key="configuration"
|
||||
@assign="async (id) => await assign(configuration, id, props.client.ClientID)"
|
||||
v-for="(peers, configuration) in assignmentStore.allConfigurationsPeers">
|
||||
</AvailablePeersGroup>
|
||||
<h6 class="text-muted m-auto" v-if="Object.keys(assignmentStore.allConfigurationsPeers).length === 0">
|
||||
<LocaleText t="No peer is available to assign"></LocaleText>
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-footer d-flex gap-2 p-3 align-items-center justify-content-end">
|
||||
<label for="availablePeerSearchString">
|
||||
<i class="bi bi-search me-2"></i>
|
||||
</label>
|
||||
<input
|
||||
id="availablePeerSearchString"
|
||||
v-model="availablePeerSearchString"
|
||||
class="form-control form-control-sm rounded-3 w-auto" type="text">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="p-3 placeholder-glow border-bottom">
|
||||
<h6 class="placeholder w-100 rounded-3"></h6>
|
||||
<div class="placeholder w-100 rounded-3" style="height: 400px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import { fetchPost } from "@/utilities/fetch"
|
||||
import {ref} from "vue";
|
||||
import { DashboardConfigurationStore } from "@/stores/DashboardConfigurationStore.js"
|
||||
|
||||
const props = defineProps(['client'])
|
||||
const deleting = ref(false)
|
||||
const confirmDelete = ref(false)
|
||||
const emits = defineEmits(['refresh'])
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore()
|
||||
const deleteClient = async () => {
|
||||
deleting.value = true
|
||||
await fetchPost("/api/clients/deleteClient", {
|
||||
ClientID: props.client.ClientID
|
||||
}, (res) => {
|
||||
deleting.value = false
|
||||
if (res.status){
|
||||
emits("deleteSuccess")
|
||||
dashboardConfigurationStore.newMessage("Server", "Delete client successfully", "success")
|
||||
}else {
|
||||
dashboardConfigurationStore.newMessage("Server", "Failed to delete client", "danger")
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-3 d-flex gap-3 flex-column border-bottom">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<h6 class="mb-0">
|
||||
<LocaleText t="Delete Client" v-if="!confirmDelete"></LocaleText>
|
||||
<LocaleText t="Are you sure to delete this client?" v-else></LocaleText>
|
||||
</h6>
|
||||
<button class="btn btn-sm bg-danger-subtle text-danger-emphasis rounded-3 ms-auto"
|
||||
v-if="!confirmDelete"
|
||||
@click="confirmDelete = true"
|
||||
>
|
||||
<i class="bi bi-trash-fill me-2"></i>
|
||||
<LocaleText t="Delete"></LocaleText>
|
||||
</button>
|
||||
|
||||
<template v-if="confirmDelete">
|
||||
<button
|
||||
@click="deleteClient"
|
||||
class="btn btn-sm bg-danger-subtle text-danger-emphasis rounded-3 ms-auto">
|
||||
<i class="bi bi-trash-fill me-2"></i>
|
||||
<LocaleText t="Yes"></LocaleText>
|
||||
</button>
|
||||
<button class="btn btn-sm bg-secondary-subtle text-secondary-emphasis rounded-3"
|
||||
v-if="confirmDelete" @click="confirmDelete = false">
|
||||
<i class="bi bi-x-lg me-2"></i>
|
||||
<LocaleText t="No"></LocaleText>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted} from "vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const props = defineProps(['groupName', 'clients', 'searchString'])
|
||||
|
||||
const getClients = computed(() => {
|
||||
const s = props.searchString.toLowerCase()
|
||||
if (!props.searchString){
|
||||
return props.clients
|
||||
}
|
||||
return props.clients.filter(
|
||||
x =>
|
||||
(x.ClientID && x.ClientID.toLowerCase().includes(s)) ||
|
||||
(x.Email && x.Email.toLowerCase().includes(s) ||
|
||||
(x.Name && x.Name.toLowerCase().includes(s)))
|
||||
)
|
||||
})
|
||||
const route = useRoute()
|
||||
onMounted(() => {
|
||||
document.querySelector(".clientList .active")?.scrollIntoView()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card rounded-0 border-0">
|
||||
<div class="card-header d-flex align-items-center rounded-0">
|
||||
<h6 class="my-2">{{ groupName }}</h6>
|
||||
<span class="badge text-bg-primary ms-auto">
|
||||
<LocaleText :t="getClients.length + ' Client' + (getClients.length > 1 ? 's': '')"></LocaleText>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush clientList">
|
||||
<RouterLink
|
||||
:key="client.ClientID"
|
||||
:id="'client_' + client.ClientID"
|
||||
active-class="active"
|
||||
:to="{ name: 'Client Viewer', params: { id: client.ClientID } }"
|
||||
class="list-group-item d-flex flex-column border-bottom list-group-item-action client"
|
||||
v-for="client in getClients" >
|
||||
<small class="text-body">
|
||||
{{ client.Email }}
|
||||
</small>
|
||||
<small class="text-muted">
|
||||
{{ client.Name ? client.Name : 'No Name'}}
|
||||
</small>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import { fetchGet, fetchPost } from "@/utilities/fetch.js"
|
||||
import {ref} from "vue";
|
||||
import {DashboardConfigurationStore } from "@/stores/DashboardConfigurationStore.js"
|
||||
import {useRouter} from "vue-router";
|
||||
const props = defineProps(['client'])
|
||||
|
||||
|
||||
const alert = ref(false)
|
||||
const alertStatus = ref(false)
|
||||
const alertMessage = ref(false)
|
||||
const resetting = ref(false)
|
||||
const store = DashboardConfigurationStore();
|
||||
const router = useRouter()
|
||||
|
||||
const getUrl = (token) => {
|
||||
const crossServer = store.getActiveCrossServer();
|
||||
if(crossServer){
|
||||
return new URL('/client/#/reset_password?token=' + token, crossServer.host).href
|
||||
}
|
||||
return new URL('/client/#/reset_password?token=' + token, window.location.href).href
|
||||
}
|
||||
|
||||
const sendResetLink = async () => {
|
||||
resetting.value = true
|
||||
let smtpReady = false;
|
||||
let token = undefined;
|
||||
await fetchPost('/api/clients/generatePasswordResetLink', {
|
||||
ClientID: props.client.ClientID
|
||||
},async (res) => {
|
||||
if (res.status){
|
||||
token = res.data
|
||||
alertStatus.value = true
|
||||
await fetchGet('/api/email/ready', {}, (res) => {
|
||||
smtpReady = res.status
|
||||
});
|
||||
if (smtpReady){
|
||||
let body = {
|
||||
"Receiver": props.client.Email,
|
||||
"Subject": "[WGDashboard | Client] Reset Password",
|
||||
"Body":
|
||||
`Hi${props.client.Name ? ' ' + props.client.Name: ''},\n\nWe received a request to reset the password for your account. You can reset your password by visiting the link below:\n\n${getUrl(token)}\n\nThis link will expire in 30 minutes for your security. If you didn’t request a password reset, you can safely ignore this email—your current password will remain unchanged.\n\nIf you need help, feel free to contact support.\n\nBest regards,\nWGDashboard`
|
||||
}
|
||||
await fetchPost('/api/email/send', body, (res) => {
|
||||
if (res.status){
|
||||
alertMessage.value = `Send email success.`
|
||||
alert.value = true;
|
||||
}else{
|
||||
alertMessage.value = `Send email failed.`
|
||||
alertStatus.value = false;
|
||||
alert.value = true;
|
||||
}
|
||||
});
|
||||
}else{
|
||||
alertMessage.value = `Please share this URL to your client to reset the password: ${getUrl(token)}`
|
||||
alert.value = true;
|
||||
|
||||
}
|
||||
}else{
|
||||
alertStatus.value = false
|
||||
alertMessage.value = res.message
|
||||
alert.value = true
|
||||
}
|
||||
})
|
||||
resetting.value = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-3 d-flex gap-3 flex-column border-bottom">
|
||||
<div class="d-flex align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<LocaleText t="Reset Password"></LocaleText>
|
||||
</h6>
|
||||
<button class="btn btn-sm bg-primary-subtle text-primary-emphasis rounded-3 ms-auto"
|
||||
@click="sendResetLink()"
|
||||
:class="{disabled: resetting}"
|
||||
>
|
||||
<i class="bi bi-send me-2"></i>
|
||||
<LocaleText t="Send Password Reset Link" v-if="!resetting"></LocaleText>
|
||||
<LocaleText t="Sending..." v-else></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
<div class="alert rounded-3 mb-0"
|
||||
:class="[alertStatus ? 'alert-success' : 'alert-danger']"
|
||||
v-if="alert">
|
||||
{{ alertMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue"
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import { fetchGet } from "@/utilities/fetch.js"
|
||||
import { DashboardConfigurationStore } from "@/stores/DashboardConfigurationStore"
|
||||
|
||||
const props = defineProps(['mode'])
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore()
|
||||
const oidcStatus = ref(false)
|
||||
const oidcStatusLoading = ref(false)
|
||||
|
||||
const getStatus = async () => {
|
||||
await fetchGet("/api/oidc/status", {
|
||||
mode: props.mode
|
||||
}, (res) => {
|
||||
oidcStatus.value = res.data
|
||||
oidcStatusLoading.value = false
|
||||
})
|
||||
}
|
||||
await getStatus()
|
||||
const toggle = async () => {
|
||||
oidcStatusLoading.value = true
|
||||
await fetchGet('/api/oidc/toggle', {
|
||||
mode: props.mode
|
||||
}, (res) => {
|
||||
if (!res.status){
|
||||
oidcStatus.value = !oidcStatus.value
|
||||
dashboardConfigurationStore.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
oidcStatusLoading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<LocaleText t="OpenID Connect (OIDC)"></LocaleText>
|
||||
</h6>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<label class="form-check-label" for="oidc_switch">
|
||||
<LocaleText :t="oidcStatus ? 'Enabled':'Disabled'"></LocaleText>
|
||||
</label>
|
||||
<input
|
||||
:disabled="oidcStatusLoading"
|
||||
v-model="oidcStatus"
|
||||
@change="toggle()"
|
||||
class="form-check-input" type="checkbox" role="switch" id="oidc_switch">
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div>-->
|
||||
<!-- <div class="alert alert-dark rounded-3 mb-0">-->
|
||||
<!-- <LocaleText t="Due to security reason, in order to edit OIDC configuration, you will need to edit "></LocaleText>-->
|
||||
<!-- <code>wg-dashboard-oidc-providers.json</code> <LocaleText t="directly, then restart WGDashboard to apply the latest settings."></LocaleText>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue"
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import OidcSettings from "@/components/clientComponents/clientSettingComponents/oidcSettings.vue";
|
||||
import { fetchGet, fetchPost } from "@/utilities/fetch.js"
|
||||
const emits = defineEmits(['close'])
|
||||
import { DashboardConfigurationStore } from "@/stores/DashboardConfigurationStore"
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore()
|
||||
const loading = ref(false)
|
||||
const values = reactive({
|
||||
enableClients: dashboardConfigurationStore.Configuration.Clients.enable
|
||||
})
|
||||
|
||||
const toggling = ref(false)
|
||||
const updateSettings = async (key: string) => {
|
||||
toggling.value = true
|
||||
await fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Clients",
|
||||
key: key,
|
||||
value: dashboardConfigurationStore.Configuration.Clients[key]
|
||||
}, async (res) => {
|
||||
await dashboardConfigurationStore.getConfiguration()
|
||||
toggling.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="position-absolute w-100 h-100 top-0 start-0 z-1 rounded-3 d-flex p-2" style="background-color: #00000070; z-index: 9999">
|
||||
<div class="card m-auto rounded-3" style="width: 700px">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-2">
|
||||
<h4 class="mb-0">
|
||||
<LocaleText t="Clients Settings"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="emits('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4 d-flex gap-3 flex-column">
|
||||
<div class="d-flex align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<LocaleText t="Client Side App"></LocaleText>
|
||||
</h6>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<label class="form-check-label" for="oidc_switch">
|
||||
<LocaleText :t="dashboardConfigurationStore.Configuration.Clients.enable ? 'Enabled':'Disabled'"></LocaleText>
|
||||
</label>
|
||||
<input
|
||||
:disabled="toggling"
|
||||
v-model="dashboardConfigurationStore.Configuration.Clients.enable"
|
||||
@change="updateSettings('enable')"
|
||||
class="form-check-input" type="checkbox" role="switch" id="oidc_switch">
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<LocaleText t="Sign Up as Local Client"></LocaleText>
|
||||
</h6>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<label class="form-check-label" for="sign_up_switch">
|
||||
<LocaleText :t="dashboardConfigurationStore.Configuration.Clients.sign_up ? 'Enabled':'Disabled'"></LocaleText>
|
||||
</label>
|
||||
<input
|
||||
:disabled="toggling"
|
||||
v-model="dashboardConfigurationStore.Configuration.Clients.sign_up"
|
||||
@change="updateSettings('sign_up')"
|
||||
class="form-check-input" type="checkbox" role="switch" id="sign_up_switch">
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted mb-0">
|
||||
<LocaleText t="Allow clients to sign up with Email and Password"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<OidcSettings mode="Client"></OidcSettings>
|
||||
<small class="text-muted mb-0">
|
||||
<LocaleText t="Allow clients to access with OpenID"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts" async>
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import { fetchGet, fetchPost } from "@/utilities/fetch.js"
|
||||
|
||||
|
||||
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
|
||||
import { DashboardConfigurationStore } from "@/stores/DashboardConfigurationStore.js"
|
||||
|
||||
import {computed, reactive, ref, watch} from "vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import ClientAssignedPeers from "@/components/clientComponents/clientAssignedPeers.vue";
|
||||
import ClientResetPassword from "@/components/clientComponents/clientResetPassword.vue";
|
||||
import ClientDelete from "@/components/clientComponents/clientDelete.vue";
|
||||
const assignmentStore = DashboardClientAssignmentStore()
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const client = computed(() => {
|
||||
return assignmentStore.getClientById(route.params.id)
|
||||
})
|
||||
const clientAssignedPeers = ref({})
|
||||
const getAssignedPeers = async () => {
|
||||
await fetchGet('/api/clients/assignedPeers', {
|
||||
ClientID: client.value.ClientID
|
||||
}, (res) => {
|
||||
clientAssignedPeers.value = res.data;
|
||||
})
|
||||
}
|
||||
const emits = defineEmits(['deleteSuccess'])
|
||||
|
||||
const clientProfile = reactive({
|
||||
Name: undefined
|
||||
})
|
||||
|
||||
if (client.value){
|
||||
watch(() => client.value.ClientID, async () => {
|
||||
clientProfile.Name = client.value.Name;
|
||||
await getAssignedPeers()
|
||||
})
|
||||
await getAssignedPeers()
|
||||
clientProfile.Name = client.value.Name
|
||||
}else{
|
||||
router.push('/clients')
|
||||
dashboardConfigurationStore.newMessage("WGDashboard", "Client does not exist", "danger")
|
||||
}
|
||||
|
||||
|
||||
|
||||
const updatingProfile = ref(false)
|
||||
const updateProfile = async () => {
|
||||
updatingProfile.value = true
|
||||
await fetchPost("/api/clients/updateProfileName", {
|
||||
ClientID: client.value.ClientID,
|
||||
Name: clientProfile.Name
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
client.value.Name = clientProfile.Name;
|
||||
dashboardConfigurationStore.newMessage("Server", "Client name update success", "success")
|
||||
}else{
|
||||
clientProfile.Name = client.value.Name;
|
||||
dashboardConfigurationStore.newMessage("Server", "Client name update failed", "danger")
|
||||
}
|
||||
updatingProfile.value = false
|
||||
})
|
||||
}
|
||||
const deleteSuccess = async () => {
|
||||
await router.push('/clients')
|
||||
await assignmentStore.getClients()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-body d-flex flex-column overflow-y-scroll h-100" v-if="client" :key="client.ClientID">
|
||||
<div class="p-4 border-bottom bg-body-tertiary z-0">
|
||||
<div class="mb-3 backLink">
|
||||
<RouterLink to="/clients" class="text-body text-decoration-none">
|
||||
<i class="bi bi-arrow-left me-2"></i>
|
||||
Back</RouterLink>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Email"></LocaleText>
|
||||
</small>
|
||||
<h1>
|
||||
{{ client.Email }}
|
||||
</h1>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Client ID"></LocaleText>
|
||||
</small>
|
||||
<small class="ms-auto">
|
||||
<samp>{{ client.ClientID }}</samp>
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Client Name"></LocaleText>
|
||||
</small>
|
||||
<input class="form-control form-control-sm rounded-3 ms-auto"
|
||||
style="width: 300px"
|
||||
type="text" v-model="clientProfile.Name">
|
||||
<button
|
||||
@click="updateProfile()"
|
||||
aria-label="Save Client Name"
|
||||
class="btn btn-sm rounded-3 bg-success-subtle border-success-subtle text-success-emphasis">
|
||||
<i class="bi bi-save-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1 0 0; overflow-y: scroll;">
|
||||
<ClientAssignedPeers
|
||||
@refresh="getAssignedPeers()"
|
||||
:clientAssignedPeers="clientAssignedPeers"
|
||||
:client="client"></ClientAssignedPeers>
|
||||
<!-- <ClientResetPassword-->
|
||||
<!-- :client="client" v-if="client.ClientGroup === 'Local'"></ClientResetPassword>-->
|
||||
<ClientDelete
|
||||
@deleteSuccess="deleteSuccess()"
|
||||
:client="client"></ClientDelete>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="d-flex w-100 h-100 text-muted">
|
||||
<div class="m-auto text-center">
|
||||
<h1>
|
||||
<i class="bi bi-person-x"></i>
|
||||
</h1>
|
||||
<p>
|
||||
<LocaleText t="Client does not exist"></LocaleText>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (min-width: 576px) {
|
||||
.backLink{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,184 @@
|
||||
<script setup>
|
||||
import dayjs from "dayjs";
|
||||
import {computed, ref} from "vue";
|
||||
import {fetchGet, fetchPost, getUrl} from "@/utilities/fetch.js";
|
||||
import {useRoute} from "vue-router";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
const props = defineProps(["b", "delay"])
|
||||
const deleteConfirmation = ref(false)
|
||||
const restoreConfirmation = ref(false)
|
||||
const route = useRoute()
|
||||
const emit = defineEmits(["refresh", "refreshPeersList"])
|
||||
const store = DashboardConfigurationStore()
|
||||
const loading = ref(false);
|
||||
const deleteBackup = () => {
|
||||
loading.value = true;
|
||||
fetchPost("/api/deleteWireguardConfigurationBackup", {
|
||||
ConfigurationName: route.params.id,
|
||||
BackupFileName: props.b.filename
|
||||
}, (res) => {
|
||||
loading.value = false;
|
||||
if (res.status){
|
||||
emit("refresh")
|
||||
store.newMessage("Server", "Backup deleted", "success")
|
||||
}else{
|
||||
store.newMessage("Server", "Backup failed to delete", "danger")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const restoreBackup = () => {
|
||||
loading.value = true;
|
||||
fetchPost("/api/restoreWireguardConfigurationBackup", {
|
||||
ConfigurationName: route.params.id,
|
||||
BackupFileName: props.b.filename
|
||||
}, (res) => {
|
||||
loading.value = false;
|
||||
restoreConfirmation.value = false;
|
||||
if (res.status){
|
||||
emit("refreshPeersList")
|
||||
store.newMessage("Server", "Backup restored with " + props.b.filename, "success")
|
||||
}else{
|
||||
store.newMessage("Server", "Backup failed to restore", "danger")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const downloadBackup = () => {
|
||||
window.location.href = getUrl(`/api/downloadWireguardConfigurationBackup?configurationName=${route.params.id}&backupFileName=${props.b.filename}`);
|
||||
}
|
||||
|
||||
const delaySeconds = computed(() => {
|
||||
return props.delay + 's'
|
||||
})
|
||||
|
||||
const showContent = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card my-0 rounded-3">
|
||||
<div class="card-body position-relative">
|
||||
<Transition name="zoomReversed">
|
||||
<div
|
||||
v-if="deleteConfirmation"
|
||||
class="position-absolute w-100 h-100 confirmationContainer start-0 top-0 rounded-3 d-flex p-2">
|
||||
<div class="m-auto">
|
||||
<h5>
|
||||
<LocaleText t="Are you sure to delete this backup?"></LocaleText>
|
||||
</h5>
|
||||
<div class="d-flex gap-2 align-items-center justify-content-center">
|
||||
<button class="btn btn-danger rounded-3"
|
||||
:disabled="loading"
|
||||
@click='deleteBackup()'>
|
||||
<LocaleText t="Yes"></LocaleText>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteConfirmation = false"
|
||||
:disabled="loading"
|
||||
class="btn bg-secondary-subtle text-secondary-emphasis border-secondary-subtle rounded-3">
|
||||
<LocaleText t="No"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="zoomReversed">
|
||||
<div
|
||||
v-if="restoreConfirmation"
|
||||
class="position-absolute w-100 h-100 confirmationContainer start-0 top-0 rounded-3 d-flex p-2">
|
||||
<div class="m-auto">
|
||||
<h5>
|
||||
<LocaleText t="Are you sure to restore this backup?"></LocaleText>
|
||||
</h5>
|
||||
<div class="d-flex gap-2 align-items-center justify-content-center">
|
||||
<button
|
||||
:disabled="loading"
|
||||
@click="restoreBackup()"
|
||||
class="btn btn-success rounded-3">
|
||||
<LocaleText t="Yes"></LocaleText>
|
||||
</button>
|
||||
<button
|
||||
@click="restoreConfirmation = false"
|
||||
:disabled="loading"
|
||||
class="btn bg-secondary-subtle text-secondary-emphasis border-secondary-subtle rounded-3">
|
||||
<LocaleText t="No"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="d-flex gap-3">
|
||||
<div class="d-flex flex-column">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Backup"></LocaleText>
|
||||
</small>
|
||||
<samp>{{b.filename}}</samp>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Backup Date"></LocaleText>
|
||||
</small>
|
||||
{{dayjs(b.backupDate, "YYYYMMDDHHmmss").format("YYYY-MM-DD HH:mm:ss")}}
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center ms-auto">
|
||||
<button
|
||||
@click="downloadBackup()"
|
||||
class="btn bg-primary-subtle text-primary-emphasis border-primary-subtle rounded-3 btn-sm">
|
||||
<i class="bi bi-download"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="restoreConfirmation = true"
|
||||
class="btn bg-warning-subtle text-warning-emphasis border-warning-subtle rounded-3 btn-sm">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteConfirmation = true"
|
||||
class="btn bg-danger-subtle text-danger-emphasis border-danger-subtle rounded-3 btn-sm">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="card rounded-3">
|
||||
<a role="button" class="card-header d-flex text-decoration-none align-items-center"
|
||||
:class="{'border-bottom-0': !showContent}"
|
||||
style="cursor: pointer" @click="showContent = !showContent">
|
||||
<small>.conf <LocaleText t="File"></LocaleText>
|
||||
</small>
|
||||
<i class="bi bi-chevron-down ms-auto"></i>
|
||||
</a>
|
||||
<div class="card-body" v-if="showContent">
|
||||
<textarea class="form-control rounded-3" :value="b.content"
|
||||
disabled
|
||||
style="height: 300px; font-family: var(--bs-font-monospace),sans-serif !important;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<span>
|
||||
<i class="bi bi-database me-1"></i>
|
||||
<LocaleText t="Database File"></LocaleText>
|
||||
</span>
|
||||
<i class="bi ms-auto"
|
||||
:class="[b.database ? 'text-success bi-check-circle-fill' : 'text-danger bi-x-circle-fill']"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.confirmationContainer{
|
||||
background-color: rgba(0, 0, 0, 0.53);
|
||||
z-index: 9999;
|
||||
backdrop-filter: blur(1px);
|
||||
-webkit-backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
.list1-enter-active{
|
||||
transition-delay: v-bind(delaySeconds) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
import {onBeforeUnmount, onMounted, reactive, ref} from "vue";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import {useRoute} from "vue-router";
|
||||
import dayjs from "dayjs";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import Backup from "@/components/configurationComponents/backupRestoreComponents/backup.vue";
|
||||
|
||||
const route = useRoute()
|
||||
const backups = ref([])
|
||||
const loading = ref(true)
|
||||
const emit = defineEmits(["close", "refreshPeersList"])
|
||||
|
||||
onMounted(() => {
|
||||
loadBackup();
|
||||
})
|
||||
|
||||
const loadBackup = () => {
|
||||
loading.value = true
|
||||
fetchGet("/api/getWireguardConfigurationBackup", {
|
||||
configurationName: route.params.id
|
||||
}, (res) => {
|
||||
backups.value = res.data;
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const createBackup = () => {
|
||||
fetchGet("/api/createWireguardConfigurationBackup", {
|
||||
configurationName: route.params.id
|
||||
}, (res) => {
|
||||
backups.value = res.data;
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll" ref="editConfigurationContainer">
|
||||
<div class="d-flex h-100 w-100">
|
||||
<div class="modal-dialog-centered dashboardModal w-100 h-100 overflow-x-scroll flex-column gap-3 mx-3">
|
||||
<div class="my-5 d-flex gap-3 flex-column position-relative">
|
||||
<div class="title">
|
||||
<div class="d-flex mb-3">
|
||||
<h4 class="mb-0">
|
||||
<LocaleText t="Backup & Restore"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="$emit('close')"></button>
|
||||
</div>
|
||||
<button
|
||||
@click="createBackup()"
|
||||
class="btn bg-primary-subtle text-primary-emphasis border-primary-subtle rounded-3 w-100">
|
||||
<i class="bi bi-plus-circle-fill me-2"></i>
|
||||
<LocaleText t="Create Backup"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
<div class="position-relative d-flex flex-column gap-3">
|
||||
<TransitionGroup name="list1" >
|
||||
<div class="text-center title"
|
||||
key="spinner"
|
||||
v-if="loading && backups.length === 0">
|
||||
<div class="spinner-border"></div>
|
||||
</div>
|
||||
<div class="card my-0 rounded-3"
|
||||
v-else-if="!loading && backups.length === 0"
|
||||
key="noBackups"
|
||||
>
|
||||
<div class="card-body text-center text-muted">
|
||||
<i class="bi bi-x-circle-fill me-2"></i>
|
||||
<LocaleText t="No backup yet, click the button above to create backup."></LocaleText>
|
||||
</div>
|
||||
</div>
|
||||
<Backup
|
||||
@refresh="loadBackup()"
|
||||
@refreshPeersList="emit('refreshPeersList')"
|
||||
:b="b" v-for="b in backups"
|
||||
:key="b.filename"
|
||||
></Backup>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card, .title{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 700px) {
|
||||
.card, .title{
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
|
||||
.animate__fadeInUp{
|
||||
animation-timing-function: cubic-bezier(0.42, 0, 0.22, 1.0)
|
||||
}
|
||||
|
||||
.list1-move, /* apply transition to moving elements */
|
||||
.list1-enter-active,
|
||||
.list1-leave-active {
|
||||
transition: all 0.5s cubic-bezier(0.42, 0, 0.22, 1.0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.list1-enter-from,
|
||||
.list1-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
/* ensure leaving items are taken out of layout flow so that moving
|
||||
animations can be calculated correctly. */
|
||||
.list1-leave-active {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import { fetchPost } from "@/utilities/fetch.js"
|
||||
|
||||
const props = defineProps(['configuration'])
|
||||
const description = ref(props.configuration.Info.Description)
|
||||
const showStatus = ref(false)
|
||||
const status = ref(false)
|
||||
|
||||
const updateDescription = async () => {
|
||||
await fetchPost("/api/updateWireguardConfigurationInfo", {
|
||||
Name: props.configuration.Name,
|
||||
Key: "Description",
|
||||
Value: description.value
|
||||
}, (res) => {
|
||||
status.value = res.status
|
||||
toggleStatus()
|
||||
})
|
||||
}
|
||||
|
||||
const toggleSuccess = () => {
|
||||
status.value = true
|
||||
toggleStatus()
|
||||
}
|
||||
|
||||
const toggleFail = () => {
|
||||
status.value = false
|
||||
toggleStatus()
|
||||
}
|
||||
|
||||
const toggleStatus = () => {
|
||||
showStatus.value = true
|
||||
setTimeout(() => {
|
||||
showStatus.value = false
|
||||
}, 3000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex gap-1 flex-column">
|
||||
<label for="configurationDescription">
|
||||
<small style="white-space: nowrap" class="text-muted">
|
||||
<i class="bi bi-pencil-fill me-2"></i>Notes
|
||||
</small>
|
||||
</label>
|
||||
<input type="text"
|
||||
:class="[showStatus ? [status ? 'is-valid':'is-invalid'] : undefined]"
|
||||
id="configurationDescription"
|
||||
v-model="description"
|
||||
@change="updateDescription()"
|
||||
class="form-control rounded-3 bg-transparent form-control-sm">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script setup>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {onMounted, ref, useTemplateRef} from "vue";
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
const route = useRoute()
|
||||
const configurationName = route.params.id;
|
||||
const input = ref("")
|
||||
const router = useRouter()
|
||||
const store = DashboardConfigurationStore()
|
||||
const deleting = ref(false)
|
||||
|
||||
const deleteConfiguration = () => {
|
||||
clearInterval(store.Peers.RefreshInterval)
|
||||
deleting.value = true;
|
||||
fetchPost("/api/deleteWireguardConfiguration", {
|
||||
ConfigurationName: configurationName
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
router.push('/')
|
||||
store.newMessage("Server", "Configuration deleted", "success")
|
||||
}else{
|
||||
deleting.value = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const loading = ref(true)
|
||||
const backups = ref([])
|
||||
const getBackup = () => {
|
||||
loading.value = true;
|
||||
fetchGet("/api/getWireguardConfigurationBackup", {
|
||||
configurationName: configurationName
|
||||
}, (res) => {
|
||||
backups.value = res.data;
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
getBackup()
|
||||
})
|
||||
const emits = defineEmits(["backup", "close"])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal" style="width: 700px">
|
||||
<div class="card rounded-3 shadow flex-grow-1 bg-danger-subtle border-danger-subtle" id="deleteConfigurationContainer">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-0">
|
||||
<h5 class="mb-0">
|
||||
<LocaleText t="Are you sure to delete this configuration?"></LocaleText>
|
||||
</h5>
|
||||
<button type="button" class="btn-close ms-auto" @click="emits('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4 text-muted">
|
||||
<p class="mb-0">
|
||||
<LocaleText t="Once you deleted this configuration:"></LocaleText>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<LocaleText t="All connected peers will get disconnected"></LocaleText>
|
||||
</li>
|
||||
<li>
|
||||
<LocaleText t="Both configuration file (.conf) and database table related to this configuration will get deleted"></LocaleText>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="alert"
|
||||
:class="[loading ? 'alert-secondary' : (backups.length > 0 ? 'alert-success' : 'alert-danger')]">
|
||||
<div v-if="loading">
|
||||
<i class="bi bi-search me-2"></i>
|
||||
<LocaleText t="Checking backups..."></LocaleText>
|
||||
</div>
|
||||
<div v-else-if="backups.length > 0">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
<LocaleText :t="'This configuration has ' + backups.length + ' backups'"></LocaleText>
|
||||
</div>
|
||||
<div v-else class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-x-circle-fill me-2"></i>
|
||||
<LocaleText t="This configuration has no backup"></LocaleText>
|
||||
<a role="button"
|
||||
@click="emits('backup')"
|
||||
class="ms-auto btn btn-sm btn-primary rounded-3">
|
||||
<i class="bi bi-clock-history me-2"></i>
|
||||
<LocaleText t="Backup"></LocaleText>
|
||||
</a>
|
||||
<a role="button"
|
||||
@click="getBackup()"
|
||||
class="btn btn-sm btn-primary rounded-3">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<p>
|
||||
<LocaleText t="If you're sure, please type in the configuration name below and click Delete"></LocaleText>
|
||||
</p>
|
||||
<input class="form-control rounded-3 mb-3"
|
||||
:placeholder="configurationName"
|
||||
v-model="input"
|
||||
type="text">
|
||||
<button class="btn btn-danger w-100"
|
||||
@click="deleteConfiguration()"
|
||||
:disabled="input !== configurationName || deleting">
|
||||
<i class="bi bi-trash-fill me-2 rounded-3"></i>
|
||||
<LocaleText t="Delete" v-if="!deleting"></LocaleText>
|
||||
<LocaleText t="Deleting..." v-else></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,270 @@
|
||||
<script setup>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {reactive, ref, watch} from "vue";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import UpdateConfigurationName
|
||||
from "@/components/configurationComponents/editConfigurationComponents/updateConfigurationName.vue";
|
||||
import EditRawConfigurationFile
|
||||
from "@/components/configurationComponents/editConfigurationComponents/editRawConfigurationFile.vue";
|
||||
import DeleteConfiguration from "@/components/configurationComponents/deleteConfiguration.vue";
|
||||
import ConfigurationBackupRestore from "@/components/configurationComponents/configurationBackupRestore.vue";
|
||||
import EditPeerSettingsOverride
|
||||
from "@/components/configurationComponents/editConfigurationComponents/editPeerSettingsOverride.vue";
|
||||
const props = defineProps({
|
||||
configurationInfo: Object
|
||||
})
|
||||
const wgStore = WireguardConfigurationsStore()
|
||||
const store = DashboardConfigurationStore()
|
||||
const saving = ref(false)
|
||||
const data = reactive(JSON.parse(JSON.stringify(props.configurationInfo)))
|
||||
const editPrivateKey = ref(false)
|
||||
const dataChanged = ref(false)
|
||||
const reqField = reactive({
|
||||
PrivateKey: true,
|
||||
IPAddress: true,
|
||||
ListenPort: true
|
||||
})
|
||||
const genKey = () => {
|
||||
if (wgStore.checkWGKeyLength(data.PrivateKey)){
|
||||
reqField.PrivateKey = true;
|
||||
data.PublicKey = window.wireguard.generatePublicKey(data.PrivateKey)
|
||||
}else{
|
||||
reqField.PrivateKey = false;
|
||||
}
|
||||
}
|
||||
const resetForm = () => {
|
||||
dataChanged.value = false;
|
||||
Object.assign(data, JSON.parse(JSON.stringify(props.configurationInfo)))
|
||||
}
|
||||
const emit = defineEmits(["changed", "close", "refresh", "dataChanged"])
|
||||
const saveForm = () => {
|
||||
saving.value = true
|
||||
fetchPost("/api/updateWireguardConfiguration", data, (res) => {
|
||||
saving.value = false
|
||||
if (res.status){
|
||||
store.newMessage("Server", "Configuration saved", "success")
|
||||
dataChanged.value = false
|
||||
emit("dataChanged", res.data)
|
||||
|
||||
}else{
|
||||
store.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
})
|
||||
}
|
||||
const updateConfigurationName = ref(false)
|
||||
|
||||
watch(data, () => {
|
||||
dataChanged.value = JSON.stringify(data) !== JSON.stringify(props.configurationInfo);
|
||||
}, {
|
||||
deep: true
|
||||
})
|
||||
|
||||
const editRawConfigurationFileModal = ref(false)
|
||||
const backupRestoreModal = ref(false)
|
||||
const deleteConfigurationModal = ref(false)
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0" ref="editConfigurationContainer">
|
||||
<div class="w-100 h-100 overflow-y-scroll">
|
||||
<TransitionGroup name="zoom">
|
||||
<EditRawConfigurationFile
|
||||
name="EditRawConfigurationFile"
|
||||
v-if="editRawConfigurationFileModal"
|
||||
@close="editRawConfigurationFileModal = false">
|
||||
</EditRawConfigurationFile>
|
||||
<DeleteConfiguration
|
||||
key="DeleteConfiguration"
|
||||
@backup="backupRestoreModal = true"
|
||||
@close="deleteConfigurationModal = false"
|
||||
v-if="deleteConfigurationModal">
|
||||
</DeleteConfiguration>
|
||||
<ConfigurationBackupRestore
|
||||
@close="backupRestoreModal = false"
|
||||
@refreshPeersList="emit('refresh')"
|
||||
v-if="backupRestoreModal">
|
||||
</ConfigurationBackupRestore>
|
||||
</TransitionGroup>
|
||||
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal" style="width: 700px">
|
||||
<div class="card rounded-3 shadow flex-grow-1">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4">
|
||||
<h4 class="mb-0">
|
||||
<LocaleText t="Configuration Settings"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="$emit('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4 pb-4">
|
||||
<div class="d-flex gap-2 flex-column">
|
||||
<div class="d-flex align-items-center gap-3" v-if="!updateConfigurationName">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Name"></LocaleText>
|
||||
</small>
|
||||
<small>{{data.Name}}</small>
|
||||
<button
|
||||
@click="updateConfigurationName = true"
|
||||
class="btn btn-sm bg-danger-subtle border-danger-subtle text-danger-emphasis rounded-3 ms-auto">
|
||||
<LocaleText t="Update Name"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
<UpdateConfigurationName
|
||||
@close="updateConfigurationName = false"
|
||||
:configuration-name="data.Name"
|
||||
v-if="updateConfigurationName"></UpdateConfigurationName>
|
||||
<template v-else>
|
||||
<hr>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<small class="text-muted" style="word-break: keep-all">
|
||||
<LocaleText t="Public Key"></LocaleText>
|
||||
</small>
|
||||
<small class="ms-auto" style="word-break: break-all">
|
||||
{{data.PublicKey}}
|
||||
</small>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<div class="d-flex">
|
||||
<label for="configuration_private_key" class="form-label">
|
||||
<small class="text-muted d-block">
|
||||
<LocaleText t="Private Key"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<input class="form-check-input"
|
||||
type="checkbox" role="switch" id="editPrivateKeySwitch"
|
||||
v-model="editPrivateKey"
|
||||
>
|
||||
<label class="form-check-label" for="editPrivateKeySwitch">
|
||||
<small>Edit</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving || !editPrivateKey"
|
||||
:class="{'is-invalid': !reqField.PrivateKey}"
|
||||
@keyup="genKey()"
|
||||
v-model="data.PrivateKey"
|
||||
id="configuration_private_key">
|
||||
</div>
|
||||
<div>
|
||||
<label for="configuration_ipaddress_cidr" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="IP Address/CIDR"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving"
|
||||
v-model="data.Address"
|
||||
id="configuration_ipaddress_cidr">
|
||||
</div>
|
||||
<div>
|
||||
<label for="configuration_listen_port" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Listen Port"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving"
|
||||
v-model="data.ListenPort"
|
||||
id="configuration_listen_port">
|
||||
|
||||
</div>
|
||||
<div class="accordion mt-2" id="editConfigurationOptionalAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed px-3 py-2" type="button" data-bs-toggle="collapse" data-bs-target="#editOptionalAccordionCollapse">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Optional Settings"></LocaleText>
|
||||
</small>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="editOptionalAccordionCollapse"
|
||||
class="accordion-collapse collapse" data-bs-parent="#editConfigurationOptionalAccordion">
|
||||
<div class="accordion-body d-flex flex-column gap-3">
|
||||
<div v-for="key in ['Table', 'PreUp', 'PreDown', 'PostUp', 'PostDown']">
|
||||
<label :for="'configuration_' + key" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText :t="key"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving"
|
||||
v-model="data[key]"
|
||||
:id="'configuration_' + key">
|
||||
</div>
|
||||
<div v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5']"
|
||||
v-if="configurationInfo.Protocol === 'awg'">
|
||||
<label :for="'configuration_' + key" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText :t="key"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving"
|
||||
v-model="data[key]"
|
||||
:id="'configuration_' + key">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 mt-1">
|
||||
<button class="btn btn-sm bg-secondary-subtle border-secondary-subtle text-secondary-emphasis rounded-3 shadow ms-auto"
|
||||
@click="resetForm()"
|
||||
:disabled="!dataChanged || saving">
|
||||
<i class="bi bi-arrow-clockwise me-2"></i>
|
||||
<LocaleText t="Reset"></LocaleText>
|
||||
</button>
|
||||
<button class="btn btn-sm bg-primary-subtle border-primary-subtle text-primary-emphasis rounded-3 shadow"
|
||||
:disabled="!dataChanged || saving"
|
||||
@click="saveForm()"
|
||||
>
|
||||
<i class="bi bi-save-fill me-2"></i>
|
||||
<LocaleText t="Save"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
<hr>
|
||||
<EditPeerSettingsOverride :configuration="configurationInfo"></EditPeerSettingsOverride>
|
||||
<hr>
|
||||
<h5 class="mb-3">
|
||||
<LocaleText t="Danger Zone"></LocaleText>
|
||||
</h5>
|
||||
<div class="d-flex gap-2 flex-column">
|
||||
<button
|
||||
@click="backupRestoreModal = true"
|
||||
class="btn bg-warning-subtle border-warning-subtle text-warning-emphasis rounded-3 text-start d-flex">
|
||||
<i class="bi bi-copy me-auto"></i>
|
||||
<LocaleText t="Backup & Restore"></LocaleText>
|
||||
</button>
|
||||
<button
|
||||
@click="editRawConfigurationFileModal = true"
|
||||
class="btn bg-warning-subtle border-warning-subtle text-warning-emphasis rounded-3 d-flex">
|
||||
<i class="bi bi-pen me-auto"></i>
|
||||
<LocaleText t="Edit Raw Configuration File"></LocaleText>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="deleteConfigurationModal = true"
|
||||
class="btn bg-danger-subtle border-danger-subtle text-danger-emphasis rounded-3 d-flex mt-4">
|
||||
<i class="bi bi-trash-fill me-auto"></i>
|
||||
<LocaleText t="Delete Configuration"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import { fetchPost } from "@/utilities/fetch.js"
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
const props = defineProps(['configuration'])
|
||||
const saving = ref(false)
|
||||
const overridePeerSettings = ref({...props.configuration.Info.OverridePeerSettings})
|
||||
const edited = ref(false)
|
||||
const errorMsg = ref("")
|
||||
|
||||
onMounted(() => {
|
||||
document.querySelectorAll("#editPeerSettingsOverride input").forEach(
|
||||
x => x.addEventListener("change", () => {
|
||||
edited.value = true
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const resetForm = () => {
|
||||
overridePeerSettings.value = props.configuration.Info.OverridePeerSettings
|
||||
edited.value = false
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
document.querySelectorAll("#editPeerSettingsOverride input").forEach(
|
||||
x => x.classList.remove("is-invalid", "is-valid")
|
||||
)
|
||||
await fetchPost("/api/updateWireguardConfigurationInfo", {
|
||||
Name: props.configuration.Name,
|
||||
Key: "OverridePeerSettings",
|
||||
Value: overridePeerSettings.value
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
edited.value = false
|
||||
props.configuration.Info.OverridePeerSettings = overridePeerSettings.value
|
||||
document.querySelectorAll("#editPeerSettingsOverride input").forEach(
|
||||
x => x.classList.add("is-valid")
|
||||
)
|
||||
}else{
|
||||
errorMsg.value = res.message
|
||||
document.querySelector(`#override_${res.data}`).classList.add("is-invalid")
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="editPeerSettingsOverride">
|
||||
<h5 class="mb-0">
|
||||
<LocaleText t="Override Peer Settings"></LocaleText>
|
||||
</h5>
|
||||
<h6 class="mb-3 text-muted">
|
||||
<small>
|
||||
<LocaleText t="Only apply to peers in this configuration"></LocaleText>
|
||||
</small>
|
||||
</h6>
|
||||
<div class="d-flex gap-2 flex-column">
|
||||
<div>
|
||||
<label for="override_DNS" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="DNS"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving"
|
||||
v-model="overridePeerSettings.DNS"
|
||||
id="override_DNS">
|
||||
<div class="invalid-feedback">{{ errorMsg }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="override_EndpointAllowedIPs" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Endpoint Allowed IPs"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving"
|
||||
v-model="overridePeerSettings.EndpointAllowedIPs"
|
||||
id="override_EndpointAllowedIPs">
|
||||
<div class="invalid-feedback">{{ errorMsg }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="override_ListenPort" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Listen Port"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving"
|
||||
v-model="overridePeerSettings.ListenPort"
|
||||
id="override_ListenPort">
|
||||
<div class="invalid-feedback">{{ errorMsg }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="override_MTU" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="MTU"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving"
|
||||
v-model="overridePeerSettings.MTU"
|
||||
id="override_MTU">
|
||||
<div class="invalid-feedback">{{ errorMsg }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="override_PeerRemoteEndpoint" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Peer Remote Endpoint"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving"
|
||||
v-model="overridePeerSettings.PeerRemoteEndpoint"
|
||||
id="override_PeerRemoteEndpoint">
|
||||
</div>
|
||||
<div>
|
||||
<label for="override_persistent_keepalive" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Persistent Keepalive"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="saving"
|
||||
v-model="overridePeerSettings.PersistentKeepalive"
|
||||
id="override_PersistentKeepalive">
|
||||
<div class="invalid-feedback">{{ errorMsg }}</div>
|
||||
</div>
|
||||
<div class="d-flex mt-1 gap-2">
|
||||
<button
|
||||
:class="{disabled: !edited}"
|
||||
@click="resetForm()"
|
||||
class="btn btn-sm bg-secondary-subtle border-secondary-subtle text-secondary-emphasis rounded-3 shadow ms-auto">
|
||||
<i class="bi bi-arrow-clockwise me-2"></i>
|
||||
<LocaleText t="Reset"></LocaleText>
|
||||
</button>
|
||||
<button
|
||||
:class="{disabled: !edited}"
|
||||
@click="submitForm()"
|
||||
class="btn btn-sm bg-primary-subtle border-primary-subtle text-primary-emphasis rounded-3 shadow">
|
||||
<i class="bi bi-save-fill me-2"></i>
|
||||
<LocaleText t="Save"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<script setup>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import CodeEditor from "@/utilities/simple-code-editor/CodeEditor.vue";
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {useRoute} from "vue-router";
|
||||
import {ref} from "vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
const emits = defineEmits(['close'])
|
||||
const route = useRoute()
|
||||
|
||||
const content = ref("")
|
||||
const path = ref("")
|
||||
const error = ref(false)
|
||||
const errorMessage = ref("")
|
||||
|
||||
const getRaw = async () => {
|
||||
await fetchGet('/api/getWireguardConfigurationRawFile', {
|
||||
configurationName: route.params.id
|
||||
}, (res) => {
|
||||
content.value = res.data.content
|
||||
path.value = res.data.path
|
||||
})
|
||||
}
|
||||
|
||||
await getRaw()
|
||||
const dashboardStore = DashboardConfigurationStore();
|
||||
const saving = ref(false)
|
||||
|
||||
const saveRaw = async () => {
|
||||
saving.value = true
|
||||
await fetchPost('/api/updateWireguardConfigurationRawFile', {
|
||||
configurationName: route.params.id,
|
||||
rawConfiguration: content.value
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
error.value = false
|
||||
dashboardStore.newMessage("Server", "Configuration saved", "success")
|
||||
}else{
|
||||
error.value = true
|
||||
errorMessage.value = res.message
|
||||
}
|
||||
saving.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal" style="width: 1000px">
|
||||
<div class="card rounded-3 shadow flex-grow-1" id="deleteConfigurationContainer">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-0">
|
||||
<h5 class="mb-0">
|
||||
<LocaleText t="Edit Raw Configuration File"></LocaleText>
|
||||
</h5>
|
||||
<button type="button" class="btn-close ms-auto" @click="emits('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4 d-flex flex-column gap-3">
|
||||
<div class="alert alert-danger rounded-3 mb-0" v-if="error">
|
||||
<div class="mb-2">
|
||||
<strong>
|
||||
<LocaleText t="Failed to save configuration. Please see the following error message:"></LocaleText>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="bg-body w-100 p-2 rounded-3">
|
||||
<pre>{{errorMessage}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<CodeEditor
|
||||
:disabled="true"
|
||||
:read-only="saving"
|
||||
v-model="content"
|
||||
:theme="dashboardStore.Configuration.Server.dashboard_theme === 'dark' ? 'github-dark':'github'"
|
||||
:languages="[['ini', path]]"
|
||||
width="100%" height="600px">
|
||||
</CodeEditor>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn bg-secondary-subtle border-secondary-subtle text-secondary-emphasis rounded-3 shadow ms-auto px-3 py-2"
|
||||
:disabled="saving"
|
||||
@click="getRaw()">
|
||||
<i class="bi bi-arrow-clockwise me-2"></i>
|
||||
<LocaleText t="Reset"></LocaleText>
|
||||
</button>
|
||||
<button
|
||||
@click="saveRaw()"
|
||||
:disabled="saving"
|
||||
class="btn bg-danger-subtle border-danger-subtle text-danger-emphasis rounded-3 px-3 py-2 shadow"
|
||||
>
|
||||
<i class="bi bi-save-fill me-2"></i>
|
||||
<LocaleText t="Save" v-if="!saving"></LocaleText>
|
||||
<LocaleText t="Saving..." v-else></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive, ref, watch} from "vue";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
import {useRouter} from "vue-router";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
const props = defineProps({
|
||||
configurationName: String
|
||||
})
|
||||
const emit = defineEmits(['close'])
|
||||
const newConfigurationName = reactive({
|
||||
data: "",
|
||||
valid: false
|
||||
});
|
||||
const store = WireguardConfigurationsStore()
|
||||
|
||||
onMounted(() => {
|
||||
watch(() => newConfigurationName.data, (newVal) => {
|
||||
newConfigurationName.valid = /^[a-zA-Z0-9_=+.-]{1,15}$/.test(newVal) && newVal.length > 0 && !store.Configurations.find(x => x.Name === newVal);
|
||||
})
|
||||
})
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore()
|
||||
const loading = ref(false)
|
||||
const router = useRouter()
|
||||
const rename = async () => {
|
||||
if (newConfigurationName.data){
|
||||
loading.value = true
|
||||
clearInterval(dashboardConfigurationStore.Peers.RefreshInterval)
|
||||
await fetchPost("/api/renameWireguardConfiguration", {
|
||||
ConfigurationName: props.configurationName,
|
||||
NewConfigurationName: newConfigurationName.data
|
||||
}, async (res) => {
|
||||
if (res.status){
|
||||
await store.getConfigurations()
|
||||
dashboardConfigurationStore.newMessage("Server", "Configuration renamed", "success")
|
||||
router.push(`/configuration/${newConfigurationName.data}/peers`)
|
||||
}else{
|
||||
dashboardConfigurationStore.newMessage("Server", res.message, "danger")
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card rounded-3 flex-grow-1 bg-danger-subtle border-danger-subtle border shadow">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<LocaleText t="To update this configuration's name, WGDashboard will execute the following operations:"></LocaleText>
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<LocaleText t="Duplicate current configuration's database table and .conf file with the new name"></LocaleText>
|
||||
</li>
|
||||
<li>
|
||||
<LocaleText t="Delete current configuration's database table and .conf file"></LocaleText>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="d-flex align-items-center gap-3 inputGroup">
|
||||
<input class="form-control form-control-sm rounded-3" :value="configurationName" disabled>
|
||||
<h3 class="mb-0">
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</h3>
|
||||
<input class="form-control form-control-sm rounded-3"
|
||||
id="newConfigurationName"
|
||||
:class="[newConfigurationName.data ? (newConfigurationName.valid ? 'is-valid' : 'is-invalid') : '']"
|
||||
v-model="newConfigurationName.data">
|
||||
</div>
|
||||
<div class="invalid-feedback" :class="{'d-block': !newConfigurationName.valid && newConfigurationName.data}">
|
||||
<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 class="d-flex mt-3">
|
||||
<button
|
||||
@click="emit('close')"
|
||||
class="btn btn-sm bg-secondary-subtle border-secondary-subtle text-secondary-emphasis rounded-3">
|
||||
<LocaleText t="Cancel"></LocaleText>
|
||||
</button>
|
||||
<button
|
||||
@click="rename()"
|
||||
:disabled="!newConfigurationName.data || loading"
|
||||
class="btn btn-sm btn-danger rounded-3 ms-auto">
|
||||
<LocaleText t="Save"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (max-width: 567px) {
|
||||
.inputGroup{
|
||||
flex-direction: column;
|
||||
|
||||
h3{
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,207 @@
|
||||
<script>
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
import {ref} from "vue";
|
||||
|
||||
export default {
|
||||
name: "allowedIPsInput",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
data: Object,
|
||||
saving: Boolean,
|
||||
bulk: Boolean,
|
||||
availableIp: undefined,
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
allowedIp: [],
|
||||
availableIpSearchString: "",
|
||||
customAvailableIp: "",
|
||||
allowedIpFormatError: false
|
||||
}
|
||||
},
|
||||
setup(props){
|
||||
const store = WireguardConfigurationsStore();
|
||||
const dashboardStore = DashboardConfigurationStore();
|
||||
const selectedSubnet = ref("")
|
||||
if(Object.keys(props.availableIp).length > 0){
|
||||
selectedSubnet.value = Object.keys(props.availableIp)[0]
|
||||
}
|
||||
|
||||
|
||||
return {store, dashboardStore, selectedSubnet}
|
||||
},
|
||||
computed: {
|
||||
searchAvailableIps(){
|
||||
return (this.availableIpSearchString ?
|
||||
this.availableIp[this.selectedSubnet].filter(x =>
|
||||
x.includes(this.availableIpSearchString) && !this.data.allowed_ips.includes(x)) :
|
||||
this.availableIp[this.selectedSubnet].filter(x => !this.data.allowed_ips.includes(x)))
|
||||
},
|
||||
inputGetLocale(){
|
||||
return GetLocale("Enter IP Address/CIDR")
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addAllowedIp(ip){
|
||||
let list = ip.split(',')
|
||||
for (let i = 0; i < list.length; i++){
|
||||
let ipaddress = list[i].trim();
|
||||
if(this.store.checkCIDR(ipaddress)){
|
||||
this.data.allowed_ips.push(ipaddress);
|
||||
}else{
|
||||
this.allowedIpFormatError = true;
|
||||
this.dashboardStore.newMessage('WGDashboard',
|
||||
`This Allowed IP address is invalid: ${ipaddress}`, 'danger')
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.customAvailableIp = ''
|
||||
return true;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
customAvailableIp(){
|
||||
this.allowedIpFormatError = false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.availableIp !== undefined &&
|
||||
Object.keys(this.availableIp).length > 0 && this.data.allowed_ips.length === 0){
|
||||
for (let subnet in this.availableIp){
|
||||
if (this.availableIp[subnet].length > 0){
|
||||
this.addAllowedIp(this.availableIp[subnet][0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{inactiveField: this.bulk}">
|
||||
<div class="d-flex flex-column flex-md-row mb-2">
|
||||
<label for="peer_allowed_ip_textbox" class="form-label mb-0">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Allowed IPs"></LocaleText> <code><LocaleText t="(Required)"></LocaleText></code>
|
||||
</small>
|
||||
</label>
|
||||
<div class="form-check form-switch ms-md-auto">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
v-model="this.data.allowed_ips_validation"
|
||||
role="switch" id="disableIPValidation">
|
||||
<label class="form-check-label" for="disableIPValidation">
|
||||
<small>
|
||||
<LocaleText t="Allowed IPs Validation"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div class="d-flex gap-2 flex-wrap" :class="{'mb-2': this.data.allowed_ips.length > 0}">
|
||||
<TransitionGroup name="list">
|
||||
<span class="badge rounded-pill text-bg-success" v-for="(ip, index) in this.data.allowed_ips" :key="ip">
|
||||
{{ip}}
|
||||
<a role="button" @click="this.data.allowed_ips.splice(index, 1)">
|
||||
<i class="bi bi-x-circle-fill ms-1"></i></a>
|
||||
</span>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control form-control-sm rounded-start-3"
|
||||
:placeholder="this.inputGetLocale"
|
||||
:class="{'is-invalid': this.allowedIpFormatError}"
|
||||
@keyup.enter="this.customAvailableIp ? this.addAllowedIp(this.customAvailableIp) : undefined"
|
||||
v-model="customAvailableIp"
|
||||
id="peer_allowed_ip_textbox"
|
||||
:disabled="bulk">
|
||||
<button class="btn btn-sm rounded-end-3"
|
||||
:class="[this.customAvailableIp ? 'btn-success':'btn-outline-success']"
|
||||
:disabled="bulk || !this.customAvailableIp"
|
||||
@click="this.addAllowedIp(this.customAvailableIp)"
|
||||
type="button" id="button-addon2">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
<LocaleText t="or"></LocaleText>
|
||||
</small>
|
||||
<div class="dropdown flex-grow-1">
|
||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle rounded-3 w-100"
|
||||
:disabled="!availableIp || bulk"
|
||||
data-bs-auto-close="outside"
|
||||
type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-filter-circle me-2"></i>
|
||||
<LocaleText t="Pick Available IP"></LocaleText>
|
||||
</button>
|
||||
<ul class="dropdown-menu mt-2 shadow w-100 dropdown-menu-end rounded-3 pb-0"
|
||||
v-if="this.availableIp"
|
||||
style="width: 300px !important;">
|
||||
<li>
|
||||
<div class="px-3 d-flex gap-3 align-items-center">
|
||||
<label for="availableIpSearchString" class="text-muted">
|
||||
<i class="bi bi-search"></i>
|
||||
</label>
|
||||
<input
|
||||
id="availableIpSearchString"
|
||||
class="form-control form-control-sm rounded-3"
|
||||
v-model="this.availableIpSearchString">
|
||||
</div>
|
||||
<hr class="my-2">
|
||||
<div class="px-3 overflow-x-scroll d-flex overflow-x-scroll overflow-y-hidden align-items-center gap-2">
|
||||
<small class="text-muted">Subnet</small>
|
||||
<button
|
||||
v-for="key in Object.keys(this.availableIp)"
|
||||
:key="key"
|
||||
@click="this.selectedSubnet = key"
|
||||
:class="{'bg-primary-subtle': this.selectedSubnet === key}"
|
||||
class="btn btn-sm text-primary-emphasis rounded-3">
|
||||
{{key}}
|
||||
</button>
|
||||
</div>
|
||||
<hr class="mt-2 mb-0">
|
||||
</li>
|
||||
<li>
|
||||
<div class="overflow-y-scroll" style="height: 270px;">
|
||||
<div v-for="ip in this.searchAvailableIps" style="" :key="ip">
|
||||
<a class="dropdown-item d-flex" role="button" @click="this.addAllowedIp(ip)">
|
||||
<span class="me-auto"><small>{{ip}}</small></span>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="this.searchAvailableIps.length === 0" class="px-3 py-2">
|
||||
<small class="text-muted" v-if="this.availableIpSearchString"><LocaleText t="No available IP containing"></LocaleText>"{{this.availableIpSearchString}}"</small>
|
||||
<small class="text-muted" v-else><LocaleText t="No more IP address available in this subnet"></LocaleText></small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.list-move, /* apply transition to moving elements */
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
/* ensure leaving items are taken out of layout flow so that moving
|
||||
animations can be calculated correctly. */
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
import {useRoute} from "vue-router";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
|
||||
export default {
|
||||
name: "bulkAdd",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
saving: Boolean,
|
||||
data: Object,
|
||||
availableIp: undefined
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
numberOfAvailableIPs: null
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
bulkAddGetLocale(){
|
||||
return GetLocale("How many peers you want to add?")
|
||||
},
|
||||
getNumberOfAvailableIPs(){
|
||||
if (!this.numberOfAvailableIPs){
|
||||
return '...'
|
||||
}else{
|
||||
return Object.values(this.numberOfAvailableIPs).reduce((x, y) => {
|
||||
return x + y
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'data.bulkAdd': {
|
||||
immediate: true,
|
||||
handler(newVal){
|
||||
if (newVal){
|
||||
fetchGet("/api/getNumberOfAvailableIPs/" + this.$route.params.id, {}, (res) => {
|
||||
if (res.status){
|
||||
this.numberOfAvailableIPs = res.data
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="form-check form-switch ">
|
||||
<input class="form-check-input"
|
||||
type="checkbox" role="switch"
|
||||
:disabled="!this.availableIp"
|
||||
id="bulk_add" v-model="this.data.bulkAdd">
|
||||
<label class="form-check-label me-2" for="bulk_add">
|
||||
<small><strong>
|
||||
<LocaleText t="Bulk Add"></LocaleText>
|
||||
</strong></small>
|
||||
</label>
|
||||
</div>
|
||||
<p :class="{'mb-0': !this.data.bulkAdd}"><small class="text-muted d-block">
|
||||
<LocaleText t="By adding peers by bulk, each peer's name will be auto generated, and Allowed IP will be assign to the next available IP."></LocaleText>
|
||||
</small></p>
|
||||
|
||||
<div class="form-group" v-if="this.data.bulkAdd">
|
||||
<input class="form-control form-control-sm rounded-3 mb-1" type="number" min="1"
|
||||
id="bulk_add_count"
|
||||
:max="this.availableIp.length"
|
||||
v-model="this.data.bulkAddAmount"
|
||||
:placeholder="this.bulkAddGetLocale">
|
||||
<small class="text-muted">
|
||||
<LocaleText :t="`You can add up to ` +
|
||||
getNumberOfAvailableIPs
|
||||
+ ' peers'"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script>
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "dnsInput",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
|
||||
data: Object,
|
||||
saving: Boolean,
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
error: false,
|
||||
dns: JSON.parse(JSON.stringify(this.data.DNS)),
|
||||
}
|
||||
},
|
||||
setup(){
|
||||
const store = WireguardConfigurationsStore();
|
||||
const dashboardStore = DashboardConfigurationStore();
|
||||
return {store, dashboardStore}
|
||||
},
|
||||
methods:{
|
||||
checkDNS(){
|
||||
if(this.dns){
|
||||
let i = this.dns.split(',').map(x => x.replaceAll(' ', ''));
|
||||
for(let ip in i){
|
||||
if (!this.store.regexCheckIP(i[ip])){
|
||||
if (!this.error){
|
||||
this.dashboardStore.newMessage("WGDashboard", "DNS format is incorrect", "danger");
|
||||
}
|
||||
this.error = true;
|
||||
this.data.DNS = "";
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.error = false;
|
||||
this.data.DNS = this.dns;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'dns'(){
|
||||
this.checkDNS();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label for="peer_DNS_textbox" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="DNS"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:class="{'is-invalid': this.error}"
|
||||
:disabled="this.saving"
|
||||
v-model="this.dns"
|
||||
|
||||
id="peer_DNS_textbox">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "endpointAllowedIps",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
data: Object,
|
||||
saving: Boolean
|
||||
},
|
||||
setup(){
|
||||
const store = WireguardConfigurationsStore();
|
||||
const dashboardStore = DashboardConfigurationStore()
|
||||
return {store, dashboardStore}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
endpointAllowedIps: JSON.parse(JSON.stringify(this.data.endpoint_allowed_ip)),
|
||||
error: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkAllowedIP(){
|
||||
let i = this.endpointAllowedIps.split(",").map(x => x.replaceAll(' ', ''));
|
||||
for (let ip in i){
|
||||
if (!this.store.checkCIDR(i[ip])){
|
||||
if (!this.error){
|
||||
this.dashboardStore.newMessage("WGDashboard", "Endpoint Allowed IPs format is incorrect", "danger")
|
||||
}
|
||||
this.data.endpoint_allowed_ip = "";
|
||||
this.error = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.error = false;
|
||||
this.data.endpoint_allowed_ip = this.endpointAllowedIps;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'endpointAllowedIps'(){
|
||||
this.checkAllowedIP();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label for="peer_endpoint_allowed_ips" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Endpoint Allowed IPs"></LocaleText> <code><LocaleText t="(Required)"></LocaleText></code></small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:class="{'is-invalid': error}"
|
||||
:disabled="this.saving"
|
||||
v-model="this.endpointAllowedIps"
|
||||
@blur="this.checkAllowedIP()"
|
||||
id="peer_endpoint_allowed_ips">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "mtuInput",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
data: Object,
|
||||
saving: Boolean
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label for="peer_mtu" class="form-label"><small class="text-muted">
|
||||
<LocaleText t="MTU"></LocaleText>
|
||||
</small></label>
|
||||
<input type="number" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.mtu"
|
||||
min="0"
|
||||
id="peer_mtu">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "nameInput",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
bulk: Boolean,
|
||||
data: Object,
|
||||
saving: Boolean
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{inactiveField: this.bulk}">
|
||||
<label for="peer_name_textbox" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Name"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving || this.bulk"
|
||||
v-model="this.data.name"
|
||||
id="peer_name_textbox" placeholder="">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "notesInput",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
bulk: Boolean,
|
||||
data: Object,
|
||||
saving: Boolean
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{inactiveField: this.bulk}">
|
||||
<label for="peer_notes_textbox" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Notes"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving || this.bulk"
|
||||
v-model="this.data.notes"
|
||||
id="peer_notes_textbox" placeholder="">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "persistentKeepAliveInput",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
data: Object,
|
||||
saving: Boolean
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label for="peer_keep_alive" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Persistent Keepalive"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.keepalive"
|
||||
id="peer_keep_alive">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "presharedKeyInput",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
data: Object,
|
||||
saving: Boolean
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
enable: false
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
enable(){
|
||||
if (this.enable){
|
||||
this.data.preshared_key = window.wireguard.generateKeypair().presharedKey
|
||||
}else {
|
||||
this.data.preshared_key = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="d-flex align-items-start">
|
||||
<label for="peer_preshared_key_textbox" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Pre-Shared Key"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
v-model="this.enable"
|
||||
id="peer_preshared_key_switch">
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving || !this.enable"
|
||||
v-model="this.data.preshared_key"
|
||||
id="peer_preshared_key_textbox">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script>
|
||||
import "@/utilities/wireguard.js"
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
export default {
|
||||
name: "privatePublicKeyInput",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
data: Object,
|
||||
saving: Boolean,
|
||||
bulk: Boolean
|
||||
},
|
||||
setup(){
|
||||
const dashboardStore = DashboardConfigurationStore();
|
||||
const wgStore = WireguardConfigurationsStore()
|
||||
return {dashboardStore, wgStore}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
keypair: {
|
||||
publicKey: "",
|
||||
privateKey: "",
|
||||
presharedKey: ""
|
||||
},
|
||||
view: false,
|
||||
editKey: false,
|
||||
error: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
genKeyPair(){
|
||||
this.editKey = false;
|
||||
this.keypair = window.wireguard.generateKeypair();
|
||||
this.data.private_key = this.keypair.privateKey;
|
||||
this.data.public_key = this.keypair.publicKey;
|
||||
},
|
||||
testKey(key){
|
||||
const reg = /^[A-Za-z0-9+/]{43}=?=?$/;
|
||||
return reg.test(key)
|
||||
},
|
||||
checkMatching(){
|
||||
try{
|
||||
if(this.keypair.privateKey){
|
||||
if(this.wgStore.checkWGKeyLength(this.keypair.privateKey)){
|
||||
this.keypair.publicKey = window.wireguard.generatePublicKey(this.keypair.privateKey)
|
||||
if (window.wireguard.generatePublicKey(this.keypair.privateKey)
|
||||
!== this.keypair.publicKey){
|
||||
this.error = true;
|
||||
this.dashboardStore.newMessage("WGDashboard", "Private key does not match with the public key", "danger");
|
||||
}else{
|
||||
this.data.private_key = this.keypair.privateKey
|
||||
this.data.public_key = this.keypair.publicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch (e){
|
||||
this.error = true;
|
||||
this.data.private_key = "";
|
||||
this.data.public_key = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.genKeyPair();
|
||||
},
|
||||
watch: {
|
||||
keypair: {
|
||||
deep: true,
|
||||
handler(){
|
||||
this.error = false;
|
||||
this.checkMatching();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex gap-2 flex-column" :class="{inactiveField: this.bulk}">
|
||||
<div>
|
||||
<label for="peer_private_key_textbox" class="form-label d-flex align-items-center">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Private Key"></LocaleText> <code><LocaleText t="(Required for QR Code and Download)"></LocaleText></code></small>
|
||||
<a role="button" class="ms-auto text-decoration-none text-body" @click="this.view = !this.view">
|
||||
<small>
|
||||
<i class="bi me-2" :class="[this.view ? 'bi-eye-slash-fill':'bi-eye-fill']"></i><LocaleText :t="this.view ? 'Hide Keys' : 'Show Keys'"></LocaleText>
|
||||
</small>
|
||||
</a>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input :type="this.view ? 'text':'password'" class="form-control form-control-sm rounded-start-3"
|
||||
v-model="this.keypair.privateKey"
|
||||
:disabled="!this.editKey || this.bulk"
|
||||
:class="{'is-invalid': this.error, 'rounded-3': !this.view}"
|
||||
@blur="this.checkMatching()"
|
||||
id="peer_private_key_textbox">
|
||||
<button class="btn btn-outline-info btn-sm rounded-end-3"
|
||||
v-if="this.view"
|
||||
@click="this.genKeyPair()"
|
||||
:disabled="this.bulk"
|
||||
type="button" id="button-addon2">
|
||||
<i class="bi bi-arrow-repeat"></i> </button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex flex-column flex-md-row mb-2">
|
||||
<label for="public_key" class="form-label mb-0">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Public Key"></LocaleText> <code>
|
||||
<LocaleText t="(Required)"></LocaleText>
|
||||
</code></small>
|
||||
</label>
|
||||
<div class="form-check form-switch ms-md-auto">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
:disabled="this.bulk"
|
||||
id="enablePublicKeyEdit" v-model="this.editKey">
|
||||
<label class="form-check-label" for="enablePublicKeyEdit">
|
||||
<small>
|
||||
<LocaleText t="Use your own Private and Public Key"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input class="form-control-sm form-control rounded-3"
|
||||
:class="{'is-invalid': this.error}"
|
||||
v-model="this.keypair.publicKey"
|
||||
@blur="this.checkMatching()"
|
||||
:disabled="!this.editKey || this.bulk"
|
||||
:type="this.view ? 'text':'password'" id="public_key">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<script>
|
||||
import { ref } from 'vue'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import "animate.css"
|
||||
import PeerSettingsDropdown from "@/components/configurationComponents/peerSettingsDropdown.vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
import PeerTagBadge from "@/components/configurationComponents/peerTagBadge.vue";
|
||||
|
||||
export default {
|
||||
name: "peer",
|
||||
methods: {GetLocale},
|
||||
components: {
|
||||
PeerTagBadge, LocaleText, PeerSettingsDropdown
|
||||
},
|
||||
props: {
|
||||
Peer: Object, ConfigurationInfo: Object, order: Number, searchPeersLength: Number
|
||||
},
|
||||
setup(){
|
||||
const target = ref(null);
|
||||
const subMenuOpened = ref(false)
|
||||
const dashboardStore = DashboardConfigurationStore()
|
||||
onClickOutside(target, event => {
|
||||
subMenuOpened.value = false;
|
||||
});
|
||||
return {target, subMenuOpened, dashboardStore}
|
||||
},
|
||||
computed: {
|
||||
getLatestHandshake(){
|
||||
if (this.Peer.latest_handshake.includes(",")){
|
||||
return this.Peer.latest_handshake.split(",")[0]
|
||||
}
|
||||
return this.Peer.latest_handshake;
|
||||
},
|
||||
getDropup(){
|
||||
return this.searchPeersLength - this.order <= 3
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card shadow-sm rounded-3 peerCard"
|
||||
:id="'peer_'+Peer.id"
|
||||
:class="{'border-warning': Peer.restricted}">
|
||||
<div>
|
||||
<div v-if="!Peer.restricted" class="card-header bg-transparent d-flex align-items-center gap-2 border-0">
|
||||
<div class="dot ms-0" :class="{active: Peer.status === 'running'}"></div>
|
||||
<div
|
||||
style="font-size: 0.8rem; color: #28a745"
|
||||
class="d-flex align-items-center"
|
||||
v-if="dashboardStore.Configuration.Server.dashboard_peer_list_display === 'list' && Peer.status === 'running'">
|
||||
<i class="bi bi-box-arrow-in-right me-2"></i>
|
||||
<span>
|
||||
{{ Peer.endpoint }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div style="font-size: 0.8rem" class="ms-auto d-flex gap-2">
|
||||
<span class="text-primary">
|
||||
<i class="bi bi-arrow-down"></i><strong>
|
||||
{{(Peer.cumu_receive + Peer.total_receive).toFixed(4)}}</strong> GB
|
||||
</span>
|
||||
<span class="text-success">
|
||||
<i class="bi bi-arrow-up"></i><strong>
|
||||
{{(Peer.cumu_sent + Peer.total_sent).toFixed(4)}}</strong> GB
|
||||
</span>
|
||||
<span class="text-secondary" v-if="Peer.latest_handshake !== 'No Handshake'">
|
||||
<i class="bi bi-arrows-angle-contract"></i>
|
||||
{{getLatestHandshake}} ago
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="border-0 card-header bg-transparent text-warning fw-bold"
|
||||
style="font-size: 0.8rem">
|
||||
<i class="bi-lock-fill me-2"></i>
|
||||
<LocaleText t="Access Restricted"></LocaleText>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-1" style="font-size: 0.9rem">
|
||||
<h6>
|
||||
{{Peer.name ? Peer.name : GetLocale('Untitled Peer')}}
|
||||
</h6>
|
||||
<div class="d-flex"
|
||||
:class="[dashboardStore.Configuration.Server.dashboard_peer_list_display === 'grid' ? 'gap-1 flex-column' : 'flex-row gap-3']">
|
||||
<div :class="{'d-flex gap-2 align-items-center' : dashboardStore.Configuration.Server.dashboard_peer_list_display === 'list'}">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Public Key"></LocaleText>
|
||||
</small>
|
||||
<small class="d-block">
|
||||
<samp>{{Peer.id}}</samp>
|
||||
</small>
|
||||
</div>
|
||||
<div :class="{'d-flex gap-2 align-items-center' : dashboardStore.Configuration.Server.dashboard_peer_list_display === 'list'}">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Allowed IPs"></LocaleText>
|
||||
</small>
|
||||
<small class="d-block">
|
||||
<samp>{{Peer.allowed_ip}}</samp>
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1"
|
||||
:class="{'ms-auto': dashboardStore.Configuration.Server.dashboard_peer_list_display === 'list'}"
|
||||
>
|
||||
<PeerTagBadge :BackgroundColor="group.BackgroundColor" :GroupName="group.GroupName" :Icon="'bi-' + group.Icon"
|
||||
v-for="group in Object.values(ConfigurationInfo.Info.PeerGroups).filter(x => x.Peers.includes(Peer.id))"
|
||||
></PeerTagBadge>
|
||||
<div class="ms-auto px-2 rounded-3 subMenuBtn position-relative"
|
||||
:class="{active: this.subMenuOpened}"
|
||||
>
|
||||
<a role="button" class="text-body"
|
||||
@click="this.subMenuOpened = true">
|
||||
<h5 class="mb-0"><i class="bi bi-three-dots"></i></h5>
|
||||
</a>
|
||||
<Transition name="slide-fade">
|
||||
<PeerSettingsDropdown
|
||||
:dropup="getDropup"
|
||||
@qrcode="this.$emit('qrcode')"
|
||||
@configurationFile="this.$emit('configurationFile')"
|
||||
@setting="this.$emit('setting')"
|
||||
@jobs="this.$emit('jobs')"
|
||||
@refresh="this.$emit('refresh')"
|
||||
@share="this.$emit('share')"
|
||||
@assign="this.$emit('assign')"
|
||||
:Peer="Peer"
|
||||
:ConfigurationInfo="ConfigurationInfo"
|
||||
v-if="this.subMenuOpened"
|
||||
ref="target"
|
||||
></PeerSettingsDropdown>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer" role="button" @click="$emit('details')" v-if="!this.Peer.restricted">
|
||||
<small class="d-flex align-items-center">
|
||||
<LocaleText t="Details"></LocaleText>
|
||||
<i class="bi bi-chevron-right ms-auto"></i>
|
||||
</small>
|
||||
</div>
|
||||
<div class="card-footer" v-else>
|
||||
<small class="d-flex align-items-center text-muted">
|
||||
<LocaleText t="Allow access to view details"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
|
||||
.subMenuBtn.active{
|
||||
background-color: #ffffff20;
|
||||
}
|
||||
|
||||
.peerCard{
|
||||
transition: box-shadow 0.1s cubic-bezier(0.82, 0.58, 0.17, 0.9);
|
||||
}
|
||||
|
||||
.peerCard:hover{
|
||||
box-shadow: var(--bs-box-shadow) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
<script setup async>
|
||||
import {computed, onMounted, ref, watch} from "vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {useRoute} from "vue-router";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import EndpointAllowedIps from "@/components/configurationComponents/newPeersComponents/endpointAllowedIps.vue";
|
||||
import AllowedIPsInput from "@/components/configurationComponents/newPeersComponents/allowedIPsInput.vue";
|
||||
import DnsInput from "@/components/configurationComponents/newPeersComponents/dnsInput.vue";
|
||||
import NameInput from "@/components/configurationComponents/newPeersComponents/nameInput.vue";
|
||||
import PrivatePublicKeyInput from "@/components/configurationComponents/newPeersComponents/privatePublicKeyInput.vue";
|
||||
import BulkAdd from "@/components/configurationComponents/newPeersComponents/bulkAdd.vue";
|
||||
import PresharedKeyInput from "@/components/configurationComponents/newPeersComponents/presharedKeyInput.vue";
|
||||
import MtuInput from "@/components/configurationComponents/newPeersComponents/mtuInput.vue";
|
||||
import PersistentKeepAliveInput
|
||||
from "@/components/configurationComponents/newPeersComponents/persistentKeepAliveInput.vue";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import NotesInput from "./newPeersComponents/notesInput.vue";
|
||||
|
||||
const dashboardStore = DashboardConfigurationStore()
|
||||
const wireguardStore = WireguardConfigurationsStore()
|
||||
const peerData = ref({
|
||||
bulkAdd: false,
|
||||
bulkAddAmount: 0,
|
||||
name: "",
|
||||
allowed_ips: [],
|
||||
private_key: "",
|
||||
public_key: "",
|
||||
DNS: dashboardStore.Configuration.Peers.peer_global_dns,
|
||||
endpoint_allowed_ip: dashboardStore.Configuration.Peers.peer_endpoint_allowed_ip,
|
||||
notes: "",
|
||||
keepalive: parseInt(dashboardStore.Configuration.Peers.peer_keep_alive),
|
||||
mtu: parseInt(dashboardStore.Configuration.Peers.peer_mtu),
|
||||
preshared_key: "",
|
||||
preshared_key_bulkAdd: false,
|
||||
allowed_ips_validation: true,
|
||||
})
|
||||
const availableIp = ref([])
|
||||
const saving = ref(false)
|
||||
|
||||
const route = useRoute()
|
||||
await fetchGet("/api/getAvailableIPs/" + route.params.id, {}, (res) => {
|
||||
if (res.status){
|
||||
availableIp.value = res.data;
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['close', 'addedPeers'])
|
||||
|
||||
const getProtocol = computed(() => {
|
||||
return wireguardStore.Configurations.find(x => x.Name === route.params.id).Protocol;
|
||||
})
|
||||
|
||||
const allRequireFieldsFilled = computed(() => {
|
||||
let status = true;
|
||||
if (peerData.value.bulkAdd){
|
||||
if(peerData.value.bulkAddAmount.length === 0 || peerData.value.bulkAddAmount > availableIp.value.length){
|
||||
status = false;
|
||||
}
|
||||
}else{
|
||||
let requireFields =
|
||||
["allowed_ips", "private_key", "public_key", "endpoint_allowed_ip", "keepalive", "mtu"]
|
||||
requireFields.forEach(x => {
|
||||
if (peerData.value[x].length === 0) status = false;
|
||||
});
|
||||
}
|
||||
return status;
|
||||
})
|
||||
|
||||
const peerCreate = () => {
|
||||
saving.value = true
|
||||
fetchPost("/api/addPeers/" + route.params.id, peerData.value, (res) => {
|
||||
if (res.status){
|
||||
dashboardStore.newMessage("Server", "Peer created successfully", "success")
|
||||
emits('addedPeers')
|
||||
}else{
|
||||
dashboardStore.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
saving.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => {
|
||||
return peerData.value.bulkAddAmount
|
||||
}, () => {
|
||||
if (peerData.value.bulkAddAmount > availableIp.value.length){
|
||||
peerData.value.bulkAddAmount = availableIp.value.length
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll" ref="editConfigurationContainer">
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal" style="width: 1000px">
|
||||
<div class="card rounded-3 shadow flex-grow-1">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4">
|
||||
<h4 class="mb-0">
|
||||
<LocaleText t="Add Peers"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="emits('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4 pb-4">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<BulkAdd :saving="saving" :data="peerData" :availableIp="availableIp"></BulkAdd>
|
||||
<template v-if="!peerData.bulkAdd">
|
||||
<hr class="mb-0 mt-2">
|
||||
<NameInput :saving="saving" :data="peerData"></NameInput>
|
||||
<NotesInput :saving="saving" :data="peerData"></NotesInput>
|
||||
<PrivatePublicKeyInput :saving="saving" :data="peerData"></PrivatePublicKeyInput>
|
||||
<AllowedIPsInput :availableIp="availableIp" :saving="saving" :data="peerData"></AllowedIPsInput>
|
||||
</template>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="accordion mb-3" id="peerAddModalAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed rounded-3" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#peerAddModalAccordionAdvancedOptions">
|
||||
<LocaleText t="Advanced Options"></LocaleText>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="peerAddModalAccordionAdvancedOptions"
|
||||
class="accordion-collapse collapse collapsed" data-bs-parent="#peerAddModalAccordion">
|
||||
<div class="accordion-body rounded-bottom-3">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<DnsInput :saving="saving" :data="peerData"></DnsInput>
|
||||
<EndpointAllowedIps :saving="saving" :data="peerData"></EndpointAllowedIps>
|
||||
<div class="row gy-3">
|
||||
<div class="col-sm" v-if="!peerData.bulkAdd">
|
||||
<PresharedKeyInput :saving="saving" :data="peerData" :bulk="peerData.bulkAdd"></PresharedKeyInput>
|
||||
</div>
|
||||
|
||||
<div class="col-sm">
|
||||
<MtuInput :saving="saving" :data="peerData"></MtuInput>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<PersistentKeepAliveInput :saving="saving" :data="peerData"></PersistentKeepAliveInput>
|
||||
</div>
|
||||
<div class="col-12" v-if="peerData.bulkAdd">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
v-model="peerData.preshared_key_bulkAdd"
|
||||
id="bullAdd_PresharedKey_Switch" checked>
|
||||
<label class="form-check-label" for="bullAdd_PresharedKey_Switch">
|
||||
<small class="fw-bold">
|
||||
<LocaleText t="Pre-Shared Key"></LocaleText> <LocaleText t="Enabled" v-if="peerData.preshared_key_bulkAdd"></LocaleText><LocaleText t="Disabled" v-else></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="d-flex mt-2">
|
||||
<button class="ms-auto btn btn-dark btn-brand rounded-3 px-3 py-2 shadow"
|
||||
:disabled="!allRequireFieldsFilled || saving"
|
||||
@click="peerCreate()"
|
||||
>
|
||||
<i class="bi bi-plus-circle-fill me-2" v-if="!saving"></i>
|
||||
<LocaleText t="Adding..." v-if="saving"></LocaleText>
|
||||
<LocaleText t="Add" v-else></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup async>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import SearchClients from "@/components/configurationComponents/peerAssignModalComponents/searchClients.vue";
|
||||
import AssignedClients from "@/components/configurationComponents/peerAssignModalComponents/assignedClients.vue";
|
||||
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
|
||||
|
||||
const props = defineProps({
|
||||
selectedPeer: Object
|
||||
})
|
||||
const emits = defineEmits([
|
||||
'close'
|
||||
])
|
||||
const assignmentStore = DashboardClientAssignmentStore()
|
||||
|
||||
if (assignmentStore.clients.length > 0){
|
||||
assignmentStore.getClients()
|
||||
}else{
|
||||
await assignmentStore.getClients()
|
||||
}
|
||||
|
||||
await assignmentStore.getAssignedClients(props.selectedPeer.configuration.Name, props.selectedPeer.id)
|
||||
const assignClient = async (clientID) => {
|
||||
await assignmentStore.assignClient(props.selectedPeer.configuration.Name, props.selectedPeer.id, clientID)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal" style="width: 700px">
|
||||
<div class="card rounded-3 shadow flex-grow-1">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4">
|
||||
<h4 class="mb-0">
|
||||
<LocaleText t="Assign Peer to Client"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="emits('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4 pb-4 d-flex gap-2 flex-column">
|
||||
<AssignedClients
|
||||
:configuration-name="props.selectedPeer.configuration.Name"
|
||||
:peer="props.selectedPeer.id"
|
||||
></AssignedClients>
|
||||
<SearchClients
|
||||
@assign="args => assignClient(args)"></SearchClients>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {ref} from "vue";
|
||||
import Assignment from "@/components/configurationComponents/peerAssignModalComponents/assignment.vue";
|
||||
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
|
||||
const emits = defineEmits(['unassign'])
|
||||
const props = defineProps(['configurationName', 'peer'])
|
||||
const assignmentStore = DashboardClientAssignmentStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-3 bg-body-tertiary rounded-3 d-flex flex-column gap-2">
|
||||
<h6 class="mb-0">
|
||||
<LocaleText t="Assigned Clients"></LocaleText>
|
||||
</h6>
|
||||
<TransitionGroup name="list" tag="div" class="position-relative">
|
||||
<Assignment :assignment="a" :key="a.AssignmentID"
|
||||
@unassign="assignmentStore.unassignClient(configurationName, peer, a.AssignmentID)"
|
||||
v-for="a in assignmentStore.assignments"></Assignment>
|
||||
</TransitionGroup>
|
||||
<div class="text-center" v-if="assignmentStore.assignments.length === 0">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="No client assigned to this peer yet"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.list-move, /* apply transition to moving elements */
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* ensure leaving items are taken out of layout flow so that moving
|
||||
animations can be calculated correctly. */
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.assignment:last-child{
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
|
||||
const props = defineProps(['assignment'])
|
||||
const emits = defineEmits(['unassign'])
|
||||
const confirmDelete = ref(false)
|
||||
const assignmentStore = DashboardClientAssignmentStore()
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-body-secondary rounded-3 text-start p-2 mb-2 assignment">
|
||||
<div class="d-flex" v-if="!confirmDelete">
|
||||
<div class="d-flex flex-column">
|
||||
<small>
|
||||
{{ assignment.Client.Email }}
|
||||
</small>
|
||||
<small class="text-muted">
|
||||
{{ assignment.Client.Name ? assignment.Client.Name + ' | ' : '' }}{{ assignment.Client.ClientGroup ? assignment.Client.ClientGroup : 'Local' }}
|
||||
</small>
|
||||
</div>
|
||||
<button
|
||||
v-if="!confirmDelete"
|
||||
@click="confirmDelete = !confirmDelete"
|
||||
:class="{disabled: assignmentStore.unassigning}"
|
||||
aria-label="Delete Assignment"
|
||||
class="btn bg-danger-subtle text-danger-emphasis ms-auto">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex gap-2" v-else>
|
||||
<div class="d-flex flex-column">
|
||||
<small>
|
||||
<LocaleText t="Are you sure to delete assignment for"></LocaleText>
|
||||
</small>
|
||||
<small class="text-muted">
|
||||
<LocaleText :t="assignment.Client.Email + ' in group ' + (assignment.Client.ClientGroup ? assignment.Client.ClientGroup : 'Local') + '?'"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
<button
|
||||
@click="emits('unassign')"
|
||||
aria-label="Delete Assignment"
|
||||
:class="{disabled: assignmentStore.unassigning}"
|
||||
class="btn bg-danger-subtle text-danger-emphasis ms-auto">
|
||||
<span class="spinner-border spinner-border-sm" v-if="assignmentStore.unassigning"></span>
|
||||
<i class="bi bi-check-lg" v-else></i>
|
||||
</button>
|
||||
<button
|
||||
:class="{disabled: assignmentStore.unassigning}"
|
||||
@click="confirmDelete = !confirmDelete"
|
||||
aria-label="Cancel Delete Assignment"
|
||||
class="btn bg-secondary-subtle text-secondary-emphasis ">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup>
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {computed, reactive, ref} from "vue";
|
||||
import SearchClientsGroup from "@/components/configurationComponents/peerAssignModalComponents/searchClientsGroup.vue";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
|
||||
|
||||
const props = defineProps(['clients', 'newAssignClients', 'assignments'])
|
||||
|
||||
const assignmentStore = DashboardClientAssignmentStore()
|
||||
|
||||
const selectedGroup = ref("")
|
||||
const searchString = ref("")
|
||||
const getSelectedGroup = computed(() => {
|
||||
if (selectedGroup.value){
|
||||
return {
|
||||
[selectedGroup.value] : assignmentStore.clients[selectedGroup.value]
|
||||
}
|
||||
}
|
||||
return assignmentStore.clients
|
||||
})
|
||||
const groupCount = reactive({})
|
||||
Object.keys(assignmentStore.clients).forEach(
|
||||
x => groupCount[x] = assignmentStore.clients[x].length
|
||||
)
|
||||
|
||||
const emits = defineEmits(['assign'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-3 bg-body-tertiary rounded-3 position-relative">
|
||||
<h6>
|
||||
<LocaleText t="Assign to Clients"></LocaleText>
|
||||
</h6>
|
||||
<label for="SearchClient" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Enter Email or Name to Search"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input class="form-control rounded-3 mb-2"
|
||||
id="SearchClient"
|
||||
v-model="searchString" type="email">
|
||||
<div class="w-100 rounded-3 d-flex flex-column gap-2 ">
|
||||
<div>
|
||||
<small class="text-muted">Groups</small>
|
||||
<div class="mt-1">
|
||||
<button
|
||||
:class="{'active': !selectedGroup}"
|
||||
@click="selectedGroup = ''"
|
||||
class="btn bg-primary-subtle text-primary-emphasis btn-sm me-2 rounded-3">
|
||||
<LocaleText t="All"></LocaleText>
|
||||
</button>
|
||||
<button
|
||||
@click="selectedGroup = groupName"
|
||||
:class="{'active': selectedGroup === groupName}"
|
||||
class="btn bg-primary-subtle text-primary-emphasis btn-sm me-2 rounded-3"
|
||||
v-for="(_, groupName) in assignmentStore.clients">
|
||||
<LocaleText :t="groupName"></LocaleText>
|
||||
<span class="ms-1 badge" :class="[ groupCount[groupName] > 0 ? 'bg-primary' : 'bg-secondary' ]">
|
||||
{{ groupCount[groupName] }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 border rounded-3 d-flex flex-column gap-2 overflow-y-scroll" style="height: 400px">
|
||||
<SearchClientsGroup
|
||||
@assign="(args) => emits('assign', args)"
|
||||
@count="(args) => groupCount[groupName] = args"
|
||||
:searchString="searchString"
|
||||
:group="group" :groupName="groupName"
|
||||
v-for="(group, groupName) in getSelectedGroup"></SearchClientsGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {DashboardClientAssignmentStore} from "@/stores/DashboardClientAssignmentStore.js";
|
||||
|
||||
const props = defineProps(['group', 'groupName', 'searchString'])
|
||||
const emits = defineEmits(['count', 'assign'])
|
||||
const assignmentStore = DashboardClientAssignmentStore()
|
||||
|
||||
const filterGroup = computed(() => {
|
||||
let g = props.group.filter(x =>
|
||||
!assignmentStore.assignments.map(a => a.Client.ClientID).includes(x.ClientID))
|
||||
if (props.searchString){
|
||||
let v = g.filter(
|
||||
x => (x.Name && x.Name.includes(props.searchString)) || (x.Email && x.Email.includes(props.searchString))
|
||||
)
|
||||
emits('count', v.length)
|
||||
return v
|
||||
}
|
||||
emits('count', g.length)
|
||||
return g
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<h6 class="mb-0">
|
||||
<small>{{groupName}}</small>
|
||||
</h6>
|
||||
<div v-if="filterGroup.length > 0" class="d-flex flex-column gap-2">
|
||||
<div class="bg-body-secondary rounded-3 text-start p-2 d-flex"
|
||||
v-for="client in filterGroup">
|
||||
<div class="d-flex flex-column">
|
||||
<small class="mb-0">
|
||||
{{ client.Email }}
|
||||
</small>
|
||||
<small class="text-muted">{{ client.Name ? client.Name : 'No Name' }}</small>
|
||||
</div>
|
||||
<button
|
||||
@click="emits('assign', client.ClientID)"
|
||||
:class="{disabled: assignmentStore.assigning}"
|
||||
class="btn bg-success-subtle text-success-emphasis ms-auto">
|
||||
<span class="spinner-border spinner-border-sm" v-if="assignmentStore.assigning === client.ClientID"></span>
|
||||
<i class="bi bi-plus-circle-fill" v-else></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<small class="text-muted">
|
||||
<LocaleText t="No result"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import QRCode from "qrcode";
|
||||
import {useRoute} from "vue-router";
|
||||
const emit = defineEmits(['close'])
|
||||
const props = defineProps({
|
||||
selectedPeer: Object
|
||||
})
|
||||
const store = DashboardConfigurationStore()
|
||||
const copied = ref(false)
|
||||
const configurationFile = ref("")
|
||||
const loading = ref(true)
|
||||
const error = ref({
|
||||
error: false,
|
||||
message: undefined
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const route = useRoute();
|
||||
fetchGet("/api/downloadPeer/"+route.params.id, {
|
||||
id: props.selectedPeer.id
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
configurationFile.value = res.data.file
|
||||
loading.value = false;
|
||||
}else{
|
||||
this.dashboardStore.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const copy = async () => {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText){
|
||||
navigator.clipboard.writeText(configurationFile.value).then(() => {
|
||||
copied.value = true;
|
||||
setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 3000)
|
||||
}).catch(() => {
|
||||
store.newMessage("WGDashboard","Failed to copy", "danger");
|
||||
})
|
||||
}else{
|
||||
const ele = document.querySelector("#peerConfigurationFile");
|
||||
ele.select();
|
||||
const successful = document.execCommand("copy");
|
||||
if (successful) {
|
||||
copied.value = true;
|
||||
setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 3000)
|
||||
} else {
|
||||
store.newMessage("WGDashboard","Failed to copy", "danger");
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0">
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal justify-content-center">
|
||||
<div class="card rounded-3 shadow w-100">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-0">
|
||||
<h4 class="mb-0">
|
||||
<LocaleText t="Peer Configuration File"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto"
|
||||
@click="emit('close')"></button>
|
||||
</div>
|
||||
<div class="card-body p-4 d-flex flex-column gap-3">
|
||||
<div style="height: 300px" class="d-flex">
|
||||
<textarea
|
||||
style="height: 300px"
|
||||
class="form-control w-100 rounded-3 animate__fadeIn animate__faster animate__animated"
|
||||
id="peerConfigurationFile"
|
||||
:class="{'d-none': loading}"
|
||||
:value="configurationFile"></textarea>
|
||||
<div class="spinner-border m-auto" role="status" v-if="loading">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<button
|
||||
@click="copy()"
|
||||
:disabled="copied || loading"
|
||||
class="ms-auto btn bg-primary-subtle border-primary-subtle text-primary-emphasis rounded-3 position-relative">
|
||||
<Transition name="slide-up" mode="out-in">
|
||||
<span v-if="!copied" class="d-block">
|
||||
<i class="bi bi-clipboard-fill"></i>
|
||||
</span>
|
||||
<span v-else class="d-block" id="check">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
</span>
|
||||
</Transition>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: all 0.2s cubic-bezier(0.42, 0, 0.22, 1);
|
||||
}
|
||||
|
||||
.slide-up-enter-from, .slide-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from{
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to{
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#check{
|
||||
animation: cubic-bezier(0.42, 0, 0.22, 1.3) 0.7s spin;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<script>
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import NameInput from "@/components/configurationComponents/newPeersComponents/nameInput.vue";
|
||||
import PrivatePublicKeyInput from "@/components/configurationComponents/newPeersComponents/privatePublicKeyInput.vue";
|
||||
import AllowedIPsInput from "@/components/configurationComponents/newPeersComponents/allowedIPsInput.vue";
|
||||
import DnsInput from "@/components/configurationComponents/newPeersComponents/dnsInput.vue";
|
||||
import EndpointAllowedIps from "@/components/configurationComponents/newPeersComponents/endpointAllowedIps.vue";
|
||||
import PresharedKeyInput from "@/components/configurationComponents/newPeersComponents/presharedKeyInput.vue";
|
||||
import MtuInput from "@/components/configurationComponents/newPeersComponents/mtuInput.vue";
|
||||
import PersistentKeepAliveInput
|
||||
from "@/components/configurationComponents/newPeersComponents/persistentKeepAliveInput.vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import BulkAdd from "@/components/configurationComponents/newPeersComponents/bulkAdd.vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "peerCreate",
|
||||
components: {
|
||||
LocaleText,
|
||||
BulkAdd,
|
||||
PersistentKeepAliveInput,
|
||||
MtuInput,
|
||||
PresharedKeyInput, EndpointAllowedIps, DnsInput, AllowedIPsInput, PrivatePublicKeyInput, NameInput},
|
||||
data(){
|
||||
return{
|
||||
data: {
|
||||
bulkAdd: false,
|
||||
bulkAddAmount: 0,
|
||||
name: "",
|
||||
allowed_ips: [],
|
||||
private_key: "",
|
||||
public_key: "",
|
||||
DNS: this.dashboardStore.Configuration.Peers.peer_global_dns,
|
||||
endpoint_allowed_ip: this.dashboardStore.Configuration.Peers.peer_endpoint_allowed_ip,
|
||||
keepalive: parseInt(this.dashboardStore.Configuration.Peers.peer_keep_alive),
|
||||
mtu: parseInt(this.dashboardStore.Configuration.Peers.peer_mtu),
|
||||
preshared_key: "",
|
||||
preshared_key_bulkAdd: false,
|
||||
},
|
||||
availableIp: undefined,
|
||||
availableIpSearchString: "",
|
||||
saving: false,
|
||||
allowedIpDropdown: undefined
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
fetchGet("/api/getAvailableIPs/" + this.$route.params.id, {}, (res) => {
|
||||
if (res.status){
|
||||
this.availableIp = res.data;
|
||||
}
|
||||
})
|
||||
},
|
||||
setup(){
|
||||
const store = WireguardConfigurationsStore();
|
||||
const dashboardStore = DashboardConfigurationStore();
|
||||
return {store, dashboardStore}
|
||||
},
|
||||
methods: {
|
||||
peerCreate(){
|
||||
this.saving = true
|
||||
fetchPost("/api/addPeers/" + this.$route.params.id, this.data, (res) => {
|
||||
if (res.status){
|
||||
this.$router.push(`/configuration/${this.$route.params.id}/peers`)
|
||||
this.dashboardStore.newMessage("Server", "Peer created successfully", "success")
|
||||
}else{
|
||||
this.dashboardStore.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
this.saving = false;
|
||||
})
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
allRequireFieldsFilled(){
|
||||
let status = true;
|
||||
if (this.data.bulkAdd){
|
||||
if(this.data.bulkAddAmount.length === 0 || this.data.bulkAddAmount > this.availableIp.length){
|
||||
status = false;
|
||||
}
|
||||
}else{
|
||||
let requireFields =
|
||||
["allowed_ips", "private_key", "public_key", "endpoint_allowed_ip", "keepalive", "mtu"]
|
||||
requireFields.forEach(x => {
|
||||
if (this.data[x].length === 0) status = false;
|
||||
});
|
||||
}
|
||||
return status;
|
||||
},
|
||||
getProtocol(){
|
||||
return this.store.Configurations.find(x => x.Name === this.$route.params.id).Protocol;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
bulkAdd(newVal){
|
||||
if(!newVal){
|
||||
this.data.bulkAddAmount = "";
|
||||
}
|
||||
},
|
||||
'data.bulkAddAmount'(){
|
||||
if (this.data.bulkAddAmount > this.availableIp.length){
|
||||
this.data.bulkAddAmount = this.availableIp.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="mb-4">
|
||||
<div class="mb-5 d-flex align-items-center gap-4">
|
||||
<RouterLink to="peers"
|
||||
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="Add Peers"></LocaleText>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<BulkAdd :saving="saving" :data="this.data" :availableIp="this.availableIp"></BulkAdd>
|
||||
<hr class="mb-0 mt-2">
|
||||
<NameInput :saving="saving" :data="this.data" v-if="!this.data.bulkAdd"></NameInput>
|
||||
<PrivatePublicKeyInput :saving="saving" :data="data" v-if="!this.data.bulkAdd"></PrivatePublicKeyInput>
|
||||
<AllowedIPsInput :availableIp="this.availableIp" :saving="saving" :data="data" v-if="!this.data.bulkAdd"></AllowedIPsInput>
|
||||
<EndpointAllowedIps :saving="saving" :data="data"></EndpointAllowedIps>
|
||||
<DnsInput :saving="saving" :data="data"></DnsInput>
|
||||
|
||||
<hr class="mb-0 mt-2">
|
||||
<div class="row gy-3">
|
||||
<div class="col-sm" v-if="!this.data.bulkAdd">
|
||||
<PresharedKeyInput :saving="saving" :data="data" :bulk="this.data.bulkAdd"></PresharedKeyInput>
|
||||
</div>
|
||||
|
||||
<div class="col-sm">
|
||||
<MtuInput :saving="saving" :data="data"></MtuInput>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<PersistentKeepAliveInput :saving="saving" :data="data"></PersistentKeepAliveInput>
|
||||
</div>
|
||||
<div class="col-12" v-if="this.data.bulkAdd">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
v-model="this.data.preshared_key_bulkAdd"
|
||||
id="bullAdd_PresharedKey_Switch" checked>
|
||||
<label class="form-check-label" for="bullAdd_PresharedKey_Switch">
|
||||
<small class="fw-bold">
|
||||
<LocaleText t="Pre-Shared Key"></LocaleText> <LocaleText t="Enabled" v-if="this.data.preshared_key_bulkAdd"></LocaleText><LocaleText t="Disabled" v-else></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex mt-2">
|
||||
<button class="ms-auto btn btn-dark btn-brand rounded-3 px-3 py-2 shadow"
|
||||
:disabled="!this.allRequireFieldsFilled || this.saving"
|
||||
@click="this.peerCreate()"
|
||||
>
|
||||
<i class="bi bi-plus-circle-fill me-2" v-if="!this.saving"></i>
|
||||
<LocaleText t="Adding..." v-if="this.saving"></LocaleText>
|
||||
<LocaleText t="Add" v-else></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.peerSettingContainer {
|
||||
background-color: #00000060;
|
||||
z-index: 9998;
|
||||
}
|
||||
|
||||
div{
|
||||
transition: 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.inactiveField{
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.card{
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,184 @@
|
||||
<script setup lang="ts">
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {ref} from "vue";
|
||||
|
||||
import {
|
||||
Chart,
|
||||
LineElement,
|
||||
BarElement,
|
||||
BarController,
|
||||
LineController,
|
||||
LinearScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
CategoryScale,
|
||||
PointElement,
|
||||
Filler
|
||||
} from 'chart.js';
|
||||
Chart.register(
|
||||
LineElement,
|
||||
BarElement,
|
||||
BarController,
|
||||
LineController,
|
||||
LinearScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
CategoryScale,
|
||||
PointElement,
|
||||
Filler
|
||||
);
|
||||
import PeerSessions from "@/components/peerDetailsModalComponents/peerSessions.vue";
|
||||
import PeerTraffics from "@/components/peerDetailsModalComponents/peerTraffics.vue";
|
||||
import PeerEndpoints from "@/components/peerDetailsModalComponents/peerEndpoints.vue";
|
||||
const props = defineProps(['selectedPeer'])
|
||||
const selectedDate = ref(undefined)
|
||||
defineEmits(['close'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll ">
|
||||
<div class="d-flex h-100 w-100 pb-2">
|
||||
<div class="m-auto w-100 p-2">
|
||||
<div class="card rounded-3 shadow h-100" >
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-2">
|
||||
<h4 class="mb-0 fw-normal">
|
||||
<LocaleText t="Peer Details"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="$emit('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small><LocaleText t="Peer" /></small></p>
|
||||
<h2 class="mb-0">{{ selectedPeer.name }}</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedPeer.notes" class="text-end">
|
||||
<p class="mb-0 text-muted"><small><LocaleText t="Notes" /></small></p>
|
||||
<p class="mb-0" style="white-space: pre-wrap">{{ selectedPeer.notes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3 gy-2 gx-2 mb-2">
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body py-2 d-flex flex-column justify-content-center">
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Status"></LocaleText>
|
||||
</small></p>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="dot ms-0 me-2" :class="{active: selectedPeer.status === 'running'}"></span>
|
||||
<LocaleText t="Connected" v-if="selectedPeer.status === 'running'"></LocaleText>
|
||||
<LocaleText t="Disconnected" v-else></LocaleText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body py-2 d-flex flex-column justify-content-center">
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Allowed IPs"></LocaleText>
|
||||
</small></p>
|
||||
{{selectedPeer.allowed_ip}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="word-break: break-all" class="col-12 col-lg-6">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body py-2 d-flex flex-column justify-content-center">
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Public Key"></LocaleText>
|
||||
</small></p>
|
||||
<samp>{{selectedPeer.id}}</samp>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Latest Handshake Time"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4">
|
||||
<LocaleText :t="selectedPeer.latest_handshake !== 'No Handshake' ? selectedPeer.latest_handshake + ' ago': 'No Handshake'"></LocaleText>
|
||||
</strong>
|
||||
</div>
|
||||
<i class="bi bi-person-raised-hand ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Total Usage"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4 text-warning">
|
||||
{{ (selectedPeer.total_data + selectedPeer.cumu_data).toFixed(4) }} GB
|
||||
</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Total Received"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4 text-primary">{{(selectedPeer.total_receive + selectedPeer.cumu_receive).toFixed(4)}} GB</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Total Sent"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4 text-success">{{(selectedPeer.total_sent + selectedPeer.cumu_sent).toFixed(4)}} GB</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<PeerTraffics
|
||||
:selectedDate="selectedDate"
|
||||
:selectedPeer="selectedPeer"></PeerTraffics>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<PeerSessions
|
||||
:selectedDate="selectedDate"
|
||||
@selectDate="args => selectedDate = args"
|
||||
:selectedPeer="selectedPeer"></PeerSessions>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<PeerEndpoints
|
||||
:selectedPeer="selectedPeer"
|
||||
>
|
||||
</PeerEndpoints>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import {onBeforeUnmount, onMounted, ref} from "vue";
|
||||
const observer = ref(undefined);
|
||||
const emits = defineEmits(['loadMore'])
|
||||
const props = defineProps(['peerListLength', 'showPeersCount'])
|
||||
|
||||
onMounted(() => {
|
||||
observer.value = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting){
|
||||
emits('loadMore');
|
||||
}
|
||||
})}, {
|
||||
rootMargin: "20px",
|
||||
threshold: 1.0,
|
||||
})
|
||||
observer.value.observe(document.querySelector("#loadMore"))
|
||||
})
|
||||
|
||||
onBeforeUnmount(() =>{
|
||||
observer.value.disconnect();
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="margin-bottom: 20px; height: 1px" id="loadMore"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script>
|
||||
import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJobsComponents/scheduleDropdown.vue";
|
||||
import SchedulePeerJob from "@/components/configurationComponents/peerScheduleJobsComponents/schedulePeerJob.vue";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {v4} from "uuid";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
export default {
|
||||
name: "peerJobs",
|
||||
setup(){
|
||||
const store = WireguardConfigurationsStore();
|
||||
return {store}
|
||||
},
|
||||
props:{
|
||||
selectedPeer: Object
|
||||
},
|
||||
components:{
|
||||
LocaleText,
|
||||
SchedulePeerJob,
|
||||
ScheduleDropdown,
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
deleteJob(j){
|
||||
this.selectedPeer.jobs = this.selectedPeer.jobs.filter(x => x.JobID !== j.JobID);
|
||||
},
|
||||
addJob(){
|
||||
this.selectedPeer.jobs.unshift(JSON.parse(JSON.stringify({
|
||||
JobID: v4().toString(),
|
||||
Configuration: this.selectedPeer.configuration.Name,
|
||||
Peer: this.selectedPeer.id,
|
||||
Field: this.store.PeerScheduleJobs.dropdowns.Field[0].value,
|
||||
Operator: this.store.PeerScheduleJobs.dropdowns.Operator[0].value,
|
||||
Value: "",
|
||||
CreationDate: "",
|
||||
ExpireDate: "",
|
||||
Action: this.store.PeerScheduleJobs.dropdowns.Action[0].value
|
||||
}))
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal">
|
||||
<div class="card rounded-3 shadow" style="width: 700px">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-2">
|
||||
<h4 class="mb-0 fw-normal">
|
||||
<LocaleText t="Schedule Jobs"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4 pb-4 pt-2 position-relative">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<button class="btn bg-primary-subtle border-1 border-primary-subtle text-primary-emphasis rounded-3 shadow"
|
||||
|
||||
@click="this.addJob()">
|
||||
<i class="bi bi-plus-lg me-2"></i>
|
||||
<LocaleText t="Job"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
<TransitionGroup name="schedulePeerJobTransition" tag="div" class="position-relative">
|
||||
<SchedulePeerJob
|
||||
@refresh="this.$emit('refresh')"
|
||||
@delete="this.deleteJob(job)"
|
||||
:dropdowns="this.store.PeerScheduleJobs.dropdowns"
|
||||
:key="job.JobID"
|
||||
:pjob="job" v-for="(job, index) in this.selectedPeer.jobs">
|
||||
</SchedulePeerJob>
|
||||
|
||||
<div class="card shadow-sm" key="none"
|
||||
style="height: 153px"
|
||||
v-if="this.selectedPeer.jobs.length === 0">
|
||||
<div class="card-body text-muted text-center d-flex">
|
||||
<h6 class="m-auto">
|
||||
<LocaleText t="This peer does not have any job yet."></LocaleText>
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.schedulePeerJobTransition-move, /* apply transition to moving elements */
|
||||
.schedulePeerJobTransition-enter-active,
|
||||
.schedulePeerJobTransition-leave-active {
|
||||
transition: all 0.4s cubic-bezier(0.82, 0.58, 0.17, 0.9);
|
||||
}
|
||||
|
||||
.schedulePeerJobTransition-enter-from,
|
||||
.schedulePeerJobTransition-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* ensure leaving items are taken out of layout flow so that moving
|
||||
animations can be calculated correctly. */
|
||||
.schedulePeerJobTransition-leave-active {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script>
|
||||
import SchedulePeerJob from "@/components/configurationComponents/peerScheduleJobsComponents/schedulePeerJob.vue";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {v4} from "uuid";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "peerJobsAllModal",
|
||||
setup(){
|
||||
const store = WireguardConfigurationsStore();
|
||||
return {store}
|
||||
},
|
||||
components: {LocaleText, SchedulePeerJob},
|
||||
props: {
|
||||
configurationPeers: Array[Object]
|
||||
},
|
||||
computed:{
|
||||
getAllJobs(){
|
||||
return this.configurationPeers.filter(x => x.jobs.length > 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal">
|
||||
<div class="card rounded-3 shadow" style="width: 900px">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-2">
|
||||
<h4 class="mb-0 fw-normal">
|
||||
<LocaleText t="All Active Jobs"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4 pb-4 pt-2 ">
|
||||
<button class="btn bg-primary-subtle border-1 border-primary-subtle text-primary-emphasis rounded-3 shadow mb-2"
|
||||
|
||||
@click="this.$emit('allLogs')">
|
||||
<i class="bi bi-clock me-2"></i>
|
||||
<LocaleText t="Logs"></LocaleText>
|
||||
</button>
|
||||
<div class="accordion" id="peerJobsLogsModalAccordion" v-if="this.getAllJobs.length > 0">
|
||||
<div class="accordion-item" v-for="(p, index) in this.getAllJobs" :key="p.id">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
:data-bs-target="'#collapse_' + index">
|
||||
<small>
|
||||
<strong>
|
||||
<span v-if="p.name">
|
||||
{{p.name}} •
|
||||
</span>
|
||||
<samp class="text-muted">{{p.id}}</samp>
|
||||
</strong>
|
||||
</small>
|
||||
</button>
|
||||
</h2>
|
||||
<div :id="'collapse_' + index" class="accordion-collapse collapse"
|
||||
data-bs-parent="#peerJobsLogsModalAccordion">
|
||||
<div class="accordion-body">
|
||||
<SchedulePeerJob
|
||||
@delete="this.$emit('refresh')"
|
||||
@refresh="this.$emit('refresh')"
|
||||
:dropdowns="this.store.PeerScheduleJobs.dropdowns"
|
||||
:viewOnly="true"
|
||||
:key="job.JobID"
|
||||
:pjob="job" v-for="job in p.jobs">
|
||||
</SchedulePeerJob>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm"
|
||||
style="height: 153px"
|
||||
v-else>
|
||||
<div class="card-body text-muted text-center d-flex">
|
||||
<span class="m-auto">
|
||||
<LocaleText t="No active job at the moment."></LocaleText>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,189 @@
|
||||
<script>
|
||||
import dayjs from "dayjs";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
export default {
|
||||
name: "peerJobsLogsModal",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
configurationInfo: Object
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
dataLoading: true,
|
||||
data: [],
|
||||
logFetchTime: undefined,
|
||||
showLogID: false,
|
||||
showJobID: true,
|
||||
showSuccessJob: true,
|
||||
showFailedJob: true,
|
||||
showLogAmount: 10
|
||||
}
|
||||
},
|
||||
async mounted(){
|
||||
await this.fetchLog();
|
||||
},
|
||||
methods: {
|
||||
async fetchLog(){
|
||||
this.dataLoading = true;
|
||||
await fetchGet(`/api/getPeerScheduleJobLogs/${this.configurationInfo.Name}`, {}, (res) => {
|
||||
this.data = res.data;
|
||||
this.logFetchTime = dayjs().format("YYYY-MM-DD HH:mm:ss")
|
||||
this.dataLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getLogs(){
|
||||
return this.data
|
||||
.filter(x => {
|
||||
|
||||
return (this.showSuccessJob &&
|
||||
["1", "true"].includes(x.Status)) || (this.showFailedJob &&
|
||||
["0", "false"].includes(x.Status))
|
||||
})
|
||||
},
|
||||
showLogs(){
|
||||
return this.getLogs.slice(0, this.showLogAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
|
||||
<div class="container-fluid d-flex h-100 w-100">
|
||||
<div class="m-auto mt-0 modal-dialog-centered dashboardModal" style="width: 100%">
|
||||
<div class="card rounded-3 shadow w-100" >
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-0">
|
||||
<h4 class="mb-0">
|
||||
<LocaleText t="Jobs Logs"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4 pb-4 pt-2">
|
||||
<div v-if="!this.dataLoading">
|
||||
<p>
|
||||
<LocaleText t="Updated at"></LocaleText>
|
||||
: {{this.logFetchTime}}</p>
|
||||
<div class="mb-2 d-flex gap-3">
|
||||
<button @click="this.fetchLog()"
|
||||
class="btn btn-sm rounded-3 shadow-sm
|
||||
text-info-emphasis bg-info-subtle border-1 border-info-subtle me-1">
|
||||
<i class="bi bi-arrow-clockwise me-2"></i>
|
||||
<LocaleText t="Refresh"></LocaleText>
|
||||
</button>
|
||||
<div class="d-flex gap-3 align-items-center">
|
||||
<span class="text-muted">
|
||||
<LocaleText t="Filter"></LocaleText>
|
||||
</span>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="this.showSuccessJob"
|
||||
id="jobLogsShowSuccessCheck">
|
||||
<label class="form-check-label" for="jobLogsShowSuccessCheck">
|
||||
<span class="badge text-success-emphasis bg-success-subtle">
|
||||
<LocaleText t="Success"></LocaleText>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="this.showFailedJob"
|
||||
id="jobLogsShowFailedCheck">
|
||||
<label class="form-check-label" for="jobLogsShowFailedCheck">
|
||||
<span class="badge text-danger-emphasis bg-danger-subtle">
|
||||
<LocaleText t="Failed"></LocaleText>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-3 align-items-center ms-auto">
|
||||
<span class="text-muted">
|
||||
<LocaleText t="Display"></LocaleText>
|
||||
</span>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
v-model="showJobID"
|
||||
id="jobLogsShowJobIDCheck">
|
||||
<label class="form-check-label" for="jobLogsShowJobIDCheck">
|
||||
<LocaleText t="Job ID"></LocaleText>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
v-model="showLogID"
|
||||
id="jobLogsShowLogIDCheck">
|
||||
<label class="form-check-label" for="jobLogsShowLogIDCheck">
|
||||
<LocaleText t="Log ID"></LocaleText>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<LocaleText t="Date"></LocaleText>
|
||||
</th>
|
||||
<th scope="col" v-if="showLogID">
|
||||
<LocaleText t="Log ID"></LocaleText>
|
||||
</th>
|
||||
<th scope="col" v-if="showJobID">
|
||||
<LocaleText t="Job ID"></LocaleText>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<LocaleText t="Status"></LocaleText>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<LocaleText t="Message"></LocaleText>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in this.showLogs" style="font-size: 0.875rem">
|
||||
<th scope="row">{{log.LogDate}}</th>
|
||||
<td v-if="showLogID"><samp class="text-muted">{{log.LogID}}</samp></td>
|
||||
<td v-if="showJobID"><samp class="text-muted">{{log.JobID}}</samp></td>
|
||||
<td>
|
||||
<span class="badge" :class="[(log.Status === '1' || log.Status === 'true') ? 'text-success-emphasis bg-success-subtle':'text-danger-emphasis bg-danger-subtle']">
|
||||
{{(log.Status === "1" || log.Status === "true") ? 'Success': 'Failed'}}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{log.Message}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
<div class="d-flex gap-2">
|
||||
<button v-if="this.getLogs.length > this.showLogAmount"
|
||||
@click="this.showLogAmount += 20"
|
||||
class="btn btn-sm rounded-3 shadow-sm
|
||||
text-primary-emphasis bg-primary-subtle border-1 border-primary-subtle">
|
||||
<i class="bi bi-chevron-down me-2"></i>
|
||||
Show More
|
||||
</button>
|
||||
<button v-if="this.showLogAmount > 20"
|
||||
@click="this.showLogAmount = 20"
|
||||
class="btn btn-sm rounded-3 shadow-sm
|
||||
text-primary-emphasis bg-primary-subtle border-1 border-primary-subtle">
|
||||
<i class="bi bi-chevron-up me-2"></i>
|
||||
Collapse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center flex-column" v-else>
|
||||
<div class="spinner-border text-body" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,776 @@
|
||||
<script>
|
||||
import PeerSearch from "@/components/configurationComponents/peerSearch.vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import Peer from "@/components/configurationComponents/peer.vue";
|
||||
import { Line, Bar } from 'vue-chartjs'
|
||||
import Fuse from "fuse.js";
|
||||
import {
|
||||
Chart,
|
||||
LineElement,
|
||||
BarElement,
|
||||
BarController,
|
||||
LineController,
|
||||
LinearScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
CategoryScale,
|
||||
PointElement
|
||||
} from 'chart.js';
|
||||
Chart.register(
|
||||
LineElement,
|
||||
BarElement,
|
||||
BarController,
|
||||
LineController,
|
||||
LinearScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
CategoryScale,
|
||||
PointElement
|
||||
);
|
||||
import dayjs from "dayjs";
|
||||
import {defineAsyncComponent, ref} from "vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import PeerRow from "@/components/configurationComponents/peerRow.vue";
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
import PeerSearchBar from "@/components/configurationComponents/peerSearchBar.vue";
|
||||
import ProtocolBadge from "@/components/protocolBadge.vue";
|
||||
import EditRawConfigurationFile
|
||||
from "@/components/configurationComponents/editConfigurationComponents/editRawConfigurationFile.vue";
|
||||
import PeerIntersectionObserver from "@/components/configurationComponents/peerIntersectionObserver.vue";
|
||||
export default {
|
||||
name: "peerList",
|
||||
components: {
|
||||
PeerIntersectionObserver,
|
||||
EditRawConfigurationFile,
|
||||
ProtocolBadge,
|
||||
PeerSearchBar,
|
||||
PeerRow,
|
||||
DeleteConfiguration:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/deleteConfiguration.vue")),
|
||||
ConfigurationBackupRestore:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/configurationBackupRestore.vue")),
|
||||
SelectPeers:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/selectPeers.vue")),
|
||||
EditConfiguration:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/editConfiguration.vue")),
|
||||
LocaleText,
|
||||
PeerShareLinkModal:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/peerShareLinkModal.vue")),
|
||||
PeerJobsLogsModal:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/peerJobsLogsModal.vue")),
|
||||
PeerJobsAllModal:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/peerJobsAllModal.vue")),
|
||||
PeerJobs:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/peerJobs.vue")),
|
||||
PeerCreate:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/peerCreate.vue")),
|
||||
PeerQRCode:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/peerQRCode.vue")),
|
||||
PeerConfigurationFile:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/peerConfigurationFile.vue")),
|
||||
PeerSettings:
|
||||
defineAsyncComponent(() => import("@/components/configurationComponents/peerSettings.vue")),
|
||||
PeerSearch,
|
||||
Peer,
|
||||
Line,
|
||||
Bar
|
||||
},
|
||||
async setup(){
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore();
|
||||
const wireguardConfigurationStore = WireguardConfigurationsStore();
|
||||
const interval = ref(undefined)
|
||||
return {dashboardConfigurationStore, wireguardConfigurationStore, interval}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
configurationToggling: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
configurationInfo: [],
|
||||
configurationPeers: [],
|
||||
historyDataSentDifference: [],
|
||||
historyDataReceivedDifference: [],
|
||||
historySentData: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Data Sent',
|
||||
data: [],
|
||||
fill: false,
|
||||
borderColor: '#198754',
|
||||
tension: 0
|
||||
},
|
||||
],
|
||||
},
|
||||
historyReceiveData: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Data Received',
|
||||
data: [],
|
||||
fill: false,
|
||||
borderColor: '#0d6efd',
|
||||
tension: 0
|
||||
},
|
||||
],
|
||||
},
|
||||
peerSetting: {
|
||||
modalOpen: false,
|
||||
selectedPeer: undefined
|
||||
},
|
||||
peerScheduleJobs:{
|
||||
modalOpen: false,
|
||||
selectedPeer: undefined
|
||||
},
|
||||
peerQRCode: {
|
||||
modalOpen: false,
|
||||
peerConfigData: undefined
|
||||
},
|
||||
peerConfigurationFile: {
|
||||
modalOpen: false,
|
||||
peerConfigData: undefined
|
||||
},
|
||||
peerCreate: {
|
||||
modalOpen: false
|
||||
},
|
||||
peerScheduleJobsAll: {
|
||||
modalOpen: false
|
||||
},
|
||||
peerScheduleJobsLogs: {
|
||||
modalOpen: false
|
||||
},
|
||||
peerShare:{
|
||||
modalOpen: false,
|
||||
selectedPeer: undefined
|
||||
},
|
||||
editConfiguration: {
|
||||
modalOpen: false
|
||||
},
|
||||
selectPeers: {
|
||||
modalOpen: false
|
||||
},
|
||||
backupRestore: {
|
||||
modalOpen: false
|
||||
},
|
||||
deleteConfiguration: {
|
||||
modalOpen: false
|
||||
},
|
||||
editRawConfigurationFile: {
|
||||
modalOpen: false
|
||||
},
|
||||
peerSearchBarShow: false,
|
||||
searchStringTimeout: undefined,
|
||||
searchString: "",
|
||||
showPeersCount: 10,
|
||||
showPeersThreshold: 10,
|
||||
observer: undefined
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route': {
|
||||
immediate: true,
|
||||
handler(){
|
||||
this.showPeersCount = 20;
|
||||
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
|
||||
this.loading = true;
|
||||
let id = this.$route.params.id;
|
||||
this.configurationInfo = [];
|
||||
this.configurationPeers = [];
|
||||
if (id){
|
||||
this.getPeers(id)
|
||||
this.setPeerInterval();
|
||||
}
|
||||
}
|
||||
},
|
||||
'dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval'(){
|
||||
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
|
||||
this.setPeerInterval();
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(){
|
||||
clearInterval(this.dashboardConfigurationStore.Peers.RefreshInterval);
|
||||
},
|
||||
methods:{
|
||||
toggle(){
|
||||
this.configurationToggling = true;
|
||||
fetchGet("/api/toggleWireguardConfiguration", {
|
||||
configurationName: this.configurationInfo.Name
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.dashboardConfigurationStore.newMessage("Server",
|
||||
`${this.configurationInfo.Name} ${res.data ? 'is on':'is off'}`, "success")
|
||||
}else{
|
||||
this.dashboardConfigurationStore.newMessage("Server",
|
||||
res.message, 'danger')
|
||||
}
|
||||
this.wireguardConfigurationStore.Configurations.find(x => x.Name === this.configurationInfo.Name).Status = res.data
|
||||
console.log(this.wireguardConfigurationStore.Configurations.find(x => x.Name === this.configurationInfo.Name).Status)
|
||||
|
||||
this.configurationInfo.Status = res.data
|
||||
this.configurationToggling = false;
|
||||
})
|
||||
},
|
||||
getPeers(id = this.$route.params.id){
|
||||
fetchGet("/api/getWireguardConfigurationInfo",
|
||||
{
|
||||
configurationName: id
|
||||
}, (res) => {
|
||||
this.configurationInfo = res.data.configurationInfo;
|
||||
this.configurationPeers = res.data.configurationPeers;
|
||||
this.configurationPeers.forEach(x => {
|
||||
x.restricted = false;
|
||||
})
|
||||
res.data.configurationRestrictedPeers.forEach(x => {
|
||||
x.restricted = true;
|
||||
this.configurationPeers.push(x)
|
||||
})
|
||||
this.loading = false;
|
||||
if (this.configurationPeers.length > 0){
|
||||
const sent = this.configurationPeers.map(x => x.total_sent + x.cumu_sent)
|
||||
.reduce((x,y) => x + y).toFixed(4);
|
||||
const receive = this.configurationPeers.map(x => x.total_receive + x.cumu_receive).reduce((x,y) => x + y).toFixed(4);
|
||||
if (
|
||||
this.historyDataSentDifference[this.historyDataSentDifference.length - 1] !== sent
|
||||
){
|
||||
if (this.historyDataSentDifference.length > 0){
|
||||
this.historySentData = {
|
||||
labels: [...this.historySentData.labels, dayjs().format("HH:mm:ss A")],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Data Sent',
|
||||
data: [...this.historySentData.datasets[0].data,
|
||||
((sent - this.historyDataSentDifference[this.historyDataSentDifference.length - 1])*1000)
|
||||
.toFixed(4)],
|
||||
fill: false,
|
||||
borderColor: '#198754',
|
||||
tension: 0
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
this.historyDataSentDifference.push(sent)
|
||||
}
|
||||
if (
|
||||
this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1] !== receive
|
||||
){
|
||||
if (this.historyDataReceivedDifference.length > 0){
|
||||
this.historyReceiveData = {
|
||||
labels: [...this.historyReceiveData.labels, dayjs().format("HH:mm:ss A")],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Data Received',
|
||||
data: [...this.historyReceiveData.datasets[0].data,
|
||||
((receive - this.historyDataReceivedDifference[this.historyDataReceivedDifference.length - 1])*1000)
|
||||
.toFixed(4)],
|
||||
fill: false,
|
||||
borderColor: '#0d6efd',
|
||||
tension: 0
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
this.historyDataReceivedDifference.push(receive)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
setPeerInterval(){
|
||||
this.dashboardConfigurationStore.Peers.RefreshInterval = setInterval(() => {
|
||||
this.getPeers()
|
||||
}, parseInt(this.dashboardConfigurationStore.Configuration.Server.dashboard_refresh_interval))
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
configurationSummary(){
|
||||
return {
|
||||
connectedPeers: this.configurationPeers.filter(x => x.status === "running").length,
|
||||
totalUsage: this.configurationPeers.length > 0 ?
|
||||
this.configurationPeers.filter(x => !x.restricted)
|
||||
.map(x => x.total_data + x.cumu_data).reduce((a, b) => a + b, 0).toFixed(4) : 0,
|
||||
totalReceive: this.configurationPeers.length > 0 ?
|
||||
this.configurationPeers.filter(x => !x.restricted)
|
||||
.map(x => x.total_receive + x.cumu_receive).reduce((a, b) => a + b, 0).toFixed(4) : 0,
|
||||
totalSent: this.configurationPeers.length > 0 ?
|
||||
this.configurationPeers.filter(x => !x.restricted)
|
||||
.map(x => x.total_sent + x.cumu_sent).reduce((a, b) => a + b, 0).toFixed(4) : 0
|
||||
}
|
||||
},
|
||||
receiveData(){
|
||||
return this.historyReceiveData
|
||||
},
|
||||
sentData(){
|
||||
return this.historySentData
|
||||
},
|
||||
individualDataUsage(){
|
||||
return {
|
||||
labels: this.configurationPeers.map(x => {
|
||||
if (x.name) return x.name
|
||||
return `Untitled Peer - ${x.id}`
|
||||
}),
|
||||
datasets: [{
|
||||
label: 'Total Data Usage',
|
||||
data: this.configurationPeers.map(x => x.cumu_data + x.total_data),
|
||||
backgroundColor: this.configurationPeers.map(x => `#0dcaf0`),
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (tooltipItem) => {
|
||||
return `${tooltipItem.formattedValue} GB`
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
individualDataUsageChartOption(){
|
||||
return {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
},
|
||||
y:{
|
||||
ticks: {
|
||||
callback: (val, index) => {
|
||||
return `${val} GB`
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
chartOptions() {
|
||||
return {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (tooltipItem) => {
|
||||
return `${tooltipItem.formattedValue} MB/s`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
},
|
||||
y:{
|
||||
ticks: {
|
||||
callback: (val, index) => {
|
||||
return `${val} MB/s`
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
searchPeers(){
|
||||
const fuse = new Fuse(this.configurationPeers, {
|
||||
keys: ["name", "id", "allowed_ip"]
|
||||
});
|
||||
|
||||
const result = this.wireguardConfigurationStore.searchString ?
|
||||
this.configurationPeers.filter(x => {
|
||||
return x.name.includes(this.wireguardConfigurationStore.searchString) ||
|
||||
x.id.includes(this.wireguardConfigurationStore.searchString) ||
|
||||
x.allowed_ip.includes(this.wireguardConfigurationStore.searchString)
|
||||
}) : this.configurationPeers;
|
||||
|
||||
if (this.dashboardConfigurationStore.Configuration.Server.dashboard_sort === "restricted"){
|
||||
return result.sort((a, b) => {
|
||||
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
|
||||
< b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort] ){
|
||||
return 1;
|
||||
}
|
||||
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
|
||||
> b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]){
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}).slice(0, this.showPeersCount);
|
||||
}
|
||||
|
||||
return result.sort((a, b) => {
|
||||
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
|
||||
< b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort] ){
|
||||
return -1;
|
||||
}
|
||||
if ( a[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]
|
||||
> b[this.dashboardConfigurationStore.Configuration.Server.dashboard_sort]){
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}).slice(0, this.showPeersCount)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!this.loading" class="container-md" id="peerList">
|
||||
<div class="d-flex align-items-sm-center flex-column flex-sm-row gap-3">
|
||||
<div>
|
||||
<div class="text-muted d-flex align-items-center gap-2">
|
||||
<h5 class="mb-0">
|
||||
<ProtocolBadge :protocol="this.configurationInfo.Protocol"></ProtocolBadge>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<h1 class="mb-0 display-4"><samp>{{this.configurationInfo.Name}}</samp></h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-sm-auto d-flex gap-2 flex-column">
|
||||
<div class="card rounded-3 bg-transparent ">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Status"></LocaleText>
|
||||
</small>
|
||||
<div class="dot ms-2" :class="{active: this.configurationInfo.Status}"></div>
|
||||
<div class="form-check form-switch mb-0 ms-auto pe-0 me-0">
|
||||
<label class="form-check-label" style="cursor: pointer" :for="'switch' + this.configurationInfo.id">
|
||||
<LocaleText t="On" v-if="this.configurationInfo.Status && !this.configurationToggling"></LocaleText>
|
||||
<LocaleText t="Off" v-else-if="!this.configurationInfo.Status && !this.configurationToggling"></LocaleText>
|
||||
<span v-if="this.configurationToggling"
|
||||
class="spinner-border spinner-border-sm ms-2" aria-hidden="true">
|
||||
</span>
|
||||
</label>
|
||||
<input class="form-check-input"
|
||||
style="cursor: pointer"
|
||||
:disabled="this.configurationToggling"
|
||||
type="checkbox" role="switch" :id="'switch' + this.configurationInfo.id"
|
||||
@change="this.toggle()"
|
||||
v-model="this.configurationInfo.Status"
|
||||
>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<RouterLink
|
||||
to="create"
|
||||
class="titleBtn py-2 text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle ">
|
||||
<i class="bi bi-plus-lg me-2"></i>
|
||||
<LocaleText t="Peer"></LocaleText>
|
||||
</RouterLink>
|
||||
<button class="titleBtn py-2 text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle "
|
||||
@click="editConfiguration.modalOpen = true"
|
||||
type="button" aria-expanded="false">
|
||||
<i class="bi bi-gear-fill me-2"></i>
|
||||
<LocaleText t="Configuration Settings"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row mt-3 gy-2 gx-2 mb-2">
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body py-2 d-flex flex-column justify-content-center">
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Address"></LocaleText>
|
||||
</small></p>
|
||||
{{this.configurationInfo.Address}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body py-2 d-flex flex-column justify-content-center">
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Listen Port"></LocaleText>
|
||||
</small></p>
|
||||
{{this.configurationInfo.ListenPort}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="word-break: break-all" class="col-12 col-lg-6">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body py-2 d-flex flex-column justify-content-center">
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Public Key"></LocaleText>
|
||||
</small></p>
|
||||
<samp>{{this.configurationInfo.PublicKey}}</samp>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gx-2 gy-2 mb-2">
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Connected Peers"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4">
|
||||
{{configurationSummary.connectedPeers}} / {{configurationPeers.length}}
|
||||
</strong>
|
||||
</div>
|
||||
<i class="bi bi-ethernet ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Total Usage"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4">{{configurationSummary.totalUsage}} GB</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Total Received"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4 text-primary">{{configurationSummary.totalReceive}} GB</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Total Sent"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4 text-success">{{configurationSummary.totalSent}} GB</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gx-2 gy-2 mb-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card rounded-3 bg-transparent " style="height: 270px">
|
||||
<div class="card-header bg-transparent border-0">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Peers Data Usage"></LocaleText>
|
||||
</small></div>
|
||||
<div class="card-body pt-1">
|
||||
<Bar
|
||||
:data="individualDataUsage"
|
||||
:options="individualDataUsageChartOption"
|
||||
style="width: 100%; height: 200px; max-height: 200px"></Bar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent " style="height: 270px">
|
||||
<div class="card-header bg-transparent border-0"><small class="text-muted">
|
||||
<LocaleText t="Real Time Received Data Usage"></LocaleText>
|
||||
</small></div>
|
||||
<div class="card-body pt-1">
|
||||
<Line
|
||||
:options="chartOptions"
|
||||
:data="receiveData"
|
||||
style="width: 100%; height: 200px; max-height: 200px"
|
||||
></Line>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent " style="height: 270px">
|
||||
<div class="card-header bg-transparent border-0"><small class="text-muted">
|
||||
<LocaleText t="Real Time Sent Data Usage"></LocaleText>
|
||||
</small></div>
|
||||
<div class="card-body pt-1">
|
||||
<Line
|
||||
:options="chartOptions"
|
||||
:data="sentData"
|
||||
style="width: 100%; height: 200px; max-height: 200px"
|
||||
></Line>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div style="margin-bottom: 80px">
|
||||
<PeerSearch
|
||||
@search="this.peerSearchBarShow = true"
|
||||
@jobsAll="this.peerScheduleJobsAll.modalOpen = true"
|
||||
@jobLogs="this.peerScheduleJobsLogs.modalOpen = true"
|
||||
@editConfiguration="this.editConfiguration.modalOpen = true"
|
||||
@selectPeers="this.selectPeers.modalOpen = true"
|
||||
@backupRestore="this.backupRestore.modalOpen = true"
|
||||
@deleteConfiguration="this.deleteConfiguration.modalOpen = true"
|
||||
:configuration="this.configurationInfo"></PeerSearch>
|
||||
<TransitionGroup name="peerList" tag="div" class="row gx-2 gy-2 z-0 position-relative">
|
||||
<div class="col-12 col-lg-6 col-xl-4"
|
||||
:key="peer.id"
|
||||
v-for="peer in this.searchPeers">
|
||||
<Peer :Peer="peer"
|
||||
@share="this.peerShare.selectedPeer = peer.id; this.peerShare.modalOpen = true;"
|
||||
@refresh="this.getPeers()"
|
||||
@jobs="peerScheduleJobs.modalOpen = true; peerScheduleJobs.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)"
|
||||
@setting="peerSetting.modalOpen = true; peerSetting.selectedPeer = this.configurationPeers.find(x => x.id === peer.id)"
|
||||
@qrcode="(file) => {this.peerQRCode.peerConfigData = file; this.peerQRCode.modalOpen = true;}"
|
||||
@configurationFile="(file) => {this.peerConfigurationFile.peerConfigData = file; this.peerConfigurationFile.modalOpen = true;}"
|
||||
></Peer>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<Transition name="slideUp">
|
||||
<PeerSearchBar
|
||||
@close="peerSearchBarShow = false"
|
||||
v-if="this.peerSearchBarShow"></PeerSearchBar>
|
||||
</Transition>
|
||||
|
||||
<Transition name="zoom">
|
||||
<PeerSettings v-if="this.peerSetting.modalOpen"
|
||||
key="settings"
|
||||
:selectedPeer="this.peerSetting.selectedPeer"
|
||||
@refresh="this.getPeers()"
|
||||
@close="this.peerSetting.modalOpen = false">
|
||||
</PeerSettings>
|
||||
</Transition>
|
||||
<Transition name="zoom">
|
||||
<PeerQRCode :peerConfigData="this.peerQRCode.peerConfigData"
|
||||
key="qrcode"
|
||||
@close="this.peerQRCode.modalOpen = false"
|
||||
v-if="peerQRCode.modalOpen"></PeerQRCode>
|
||||
</Transition>
|
||||
<Transition name="zoom">
|
||||
<PeerJobs
|
||||
@refresh="this.getPeers()"
|
||||
v-if="this.peerScheduleJobs.modalOpen"
|
||||
:selectedPeer="this.peerScheduleJobs.selectedPeer"
|
||||
@close="this.peerScheduleJobs.modalOpen = false">
|
||||
</PeerJobs>
|
||||
</Transition>
|
||||
<Transition name="zoom">
|
||||
<PeerJobsAllModal
|
||||
v-if="this.peerScheduleJobsAll.modalOpen"
|
||||
@refresh="this.getPeers()"
|
||||
@allLogs="peerScheduleJobsLogs.modalOpen = true"
|
||||
@close="this.peerScheduleJobsAll.modalOpen = false"
|
||||
:configurationPeers="this.configurationPeers"
|
||||
>
|
||||
</PeerJobsAllModal>
|
||||
</Transition>
|
||||
<Transition name="zoom">
|
||||
<PeerJobsLogsModal v-if="this.peerScheduleJobsLogs.modalOpen"
|
||||
|
||||
@close="this.peerScheduleJobsLogs.modalOpen = false"
|
||||
:configurationInfo="this.configurationInfo"
|
||||
>
|
||||
</PeerJobsLogsModal>
|
||||
</Transition>
|
||||
<Transition name="zoom">
|
||||
<PeerShareLinkModal
|
||||
v-if="this.peerShare.modalOpen"
|
||||
@close="this.peerShare.modalOpen = false; this.peerShare.selectedPeer = undefined;"
|
||||
:peer="this.configurationPeers.find(x => x.id === this.peerShare.selectedPeer)"></PeerShareLinkModal>
|
||||
</Transition>
|
||||
<Transition name="zoom">
|
||||
<EditConfiguration
|
||||
@editRaw="this.editRawConfigurationFile.modalOpen = true"
|
||||
@close="this.editConfiguration.modalOpen = false"
|
||||
@dataChanged="(d) => this.configurationInfo = d"
|
||||
@backupRestore="this.backupRestore.modalOpen = true"
|
||||
@deleteConfiguration="this.deleteConfiguration.modalOpen = true"
|
||||
:configurationInfo="this.configurationInfo"
|
||||
v-if="this.editConfiguration.modalOpen"></EditConfiguration>
|
||||
</Transition>
|
||||
<Transition name="zoom">
|
||||
<SelectPeers
|
||||
@refresh="this.getPeers()"
|
||||
v-if="this.selectPeers.modalOpen"
|
||||
:configurationPeers="this.configurationPeers"
|
||||
@close="this.selectPeers.modalOpen = false"
|
||||
></SelectPeers>
|
||||
</Transition>
|
||||
|
||||
<Transition name="zoom">
|
||||
<DeleteConfiguration
|
||||
@backup="backupRestore.modalOpen = true"
|
||||
@close="deleteConfiguration.modalOpen = false"
|
||||
v-if="deleteConfiguration.modalOpen"></DeleteConfiguration>
|
||||
</Transition>
|
||||
<Transition name="zoom">
|
||||
<EditRawConfigurationFile
|
||||
@close="editRawConfigurationFile.modalOpen = false"
|
||||
v-if="editRawConfigurationFile.modalOpen">
|
||||
</EditRawConfigurationFile>
|
||||
</Transition>
|
||||
<Transition name="zoom">
|
||||
<ConfigurationBackupRestore
|
||||
@close="backupRestore.modalOpen = false"
|
||||
@refreshPeersList="this.getPeers()"
|
||||
v-if="backupRestore.modalOpen"></ConfigurationBackupRestore>
|
||||
</Transition>
|
||||
<Transition name="zoom">
|
||||
<PeerConfigurationFile
|
||||
@close="peerConfigurationFile.modalOpen = false"
|
||||
v-if="peerConfigurationFile.modalOpen"
|
||||
:configurationFile="peerConfigurationFile.peerConfigData"
|
||||
></PeerConfigurationFile>
|
||||
</Transition>
|
||||
</div>
|
||||
<PeerIntersectionObserver
|
||||
:showPeersCount="this.showPeersCount"
|
||||
:peerListLength="this.searchPeers.length"
|
||||
@loadMore="this.showPeersCount += this.showPeersThreshold"></PeerIntersectionObserver>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.peerNav .nav-link{
|
||||
&.active{
|
||||
background-color: #efefef;
|
||||
}
|
||||
}
|
||||
|
||||
th, td{
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 576px) {
|
||||
.titleBtn{
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,517 @@
|
||||
<script setup async>
|
||||
import {computed, defineAsyncComponent, onBeforeUnmount, onMounted, ref, watch} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import ProtocolBadge from "@/components/protocolBadge.vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import PeerDataUsageCharts from "@/components/configurationComponents/peerListComponents/peerDataUsageCharts.vue";
|
||||
import PeerSearch from "@/components/configurationComponents/peerSearch.vue";
|
||||
import Peer from "@/components/configurationComponents/peer.vue";
|
||||
import PeerListModals from "@/components/configurationComponents/peerListComponents/peerListModals.vue";
|
||||
import PeerIntersectionObserver from "@/components/configurationComponents/peerIntersectionObserver.vue";
|
||||
import ConfigurationDescription from "@/components/configurationComponents/configurationDescription.vue";
|
||||
import PeerDetailsModal from "@/components/configurationComponents/peerDetailsModal.vue";
|
||||
import {parseCidr} from "cidr-tools";
|
||||
|
||||
// Async Components
|
||||
const PeerSearchBar = defineAsyncComponent(() => import("@/components/configurationComponents/peerSearchBar.vue"))
|
||||
const PeerJobsAllModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerJobsAllModal.vue"))
|
||||
const PeerJobsLogsModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerJobsLogsModal.vue"))
|
||||
const EditConfigurationModal = defineAsyncComponent(() => import("@/components/configurationComponents/editConfiguration.vue"))
|
||||
const SelectPeersModal = defineAsyncComponent(() => import("@/components/configurationComponents/selectPeers.vue"))
|
||||
const PeerAddModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerAddModal.vue"))
|
||||
|
||||
const dashboardStore = DashboardConfigurationStore()
|
||||
const wireguardConfigurationStore = WireguardConfigurationsStore()
|
||||
const route = useRoute()
|
||||
const configurationInfo = ref({})
|
||||
const configurationPeers = ref([])
|
||||
const configurationToggling = ref(false)
|
||||
const configurationModalSelectedPeer = ref({})
|
||||
const configurationModals = ref({
|
||||
peerNew: {
|
||||
modalOpen: false
|
||||
},
|
||||
peerSetting: {
|
||||
modalOpen: false,
|
||||
},
|
||||
peerScheduleJobs:{
|
||||
modalOpen: false,
|
||||
},
|
||||
peerQRCode: {
|
||||
modalOpen: false,
|
||||
},
|
||||
peerConfigurationFile: {
|
||||
modalOpen: false,
|
||||
},
|
||||
peerCreate: {
|
||||
modalOpen: false
|
||||
},
|
||||
peerScheduleJobsAll: {
|
||||
modalOpen: false
|
||||
},
|
||||
peerScheduleJobsLogs: {
|
||||
modalOpen: false
|
||||
},
|
||||
peerShare:{
|
||||
modalOpen: false,
|
||||
},
|
||||
editConfiguration: {
|
||||
modalOpen: false
|
||||
},
|
||||
selectPeers: {
|
||||
modalOpen: false
|
||||
},
|
||||
backupRestore: {
|
||||
modalOpen: false
|
||||
},
|
||||
deleteConfiguration: {
|
||||
modalOpen: false
|
||||
},
|
||||
editRawConfigurationFile: {
|
||||
modalOpen: false
|
||||
},
|
||||
assignPeer: {
|
||||
modalOpen: false
|
||||
},
|
||||
peerDetails: {
|
||||
modalOpen: false
|
||||
}
|
||||
})
|
||||
const peerSearchBar = ref(false)
|
||||
// Fetch Peer =====================================
|
||||
const fetchPeerList = async () => {
|
||||
await fetchGet("/api/getWireguardConfigurationInfo", {
|
||||
configurationName: route.params.id
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
configurationInfo.value = res.data.configurationInfo;
|
||||
configurationPeers.value = res.data.configurationPeers;
|
||||
|
||||
configurationPeers.value.forEach(p => {
|
||||
p.restricted = false
|
||||
})
|
||||
res.data.configurationRestrictedPeers.forEach(x => {
|
||||
x.restricted = true;
|
||||
configurationPeers.value.push(x)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
await fetchPeerList()
|
||||
|
||||
// Fetch Peer Interval =====================================
|
||||
const fetchPeerListInterval = ref(undefined)
|
||||
const setFetchPeerListInterval = () => {
|
||||
clearInterval(fetchPeerListInterval.value)
|
||||
fetchPeerListInterval.value = setInterval(async () => {
|
||||
await fetchPeerList()
|
||||
}, parseInt(dashboardStore.Configuration.Server.dashboard_refresh_interval))
|
||||
}
|
||||
setFetchPeerListInterval()
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(fetchPeerListInterval.value);
|
||||
fetchPeerListInterval.value = undefined;
|
||||
wireguardConfigurationStore.Filter.HiddenTags = []
|
||||
})
|
||||
|
||||
watch(() => {
|
||||
return dashboardStore.Configuration.Server.dashboard_refresh_interval
|
||||
}, () => {
|
||||
setFetchPeerListInterval()
|
||||
})
|
||||
|
||||
// Toggle Configuration Method =====================================
|
||||
const toggleConfiguration = async () => {
|
||||
configurationToggling.value = true;
|
||||
await fetchGet("/api/toggleWireguardConfiguration", {
|
||||
configurationName: configurationInfo.value.Name
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
dashboardStore.newMessage("Server",
|
||||
`${configurationInfo.value.Name} ${res.data ? 'is on':'is off'}`, "success")
|
||||
}else{
|
||||
dashboardStore.newMessage("Server", res.message, 'danger')
|
||||
}
|
||||
wireguardConfigurationStore.Configurations
|
||||
.find(x => x.Name === configurationInfo.value.Name).Status = res.data
|
||||
configurationInfo.value.Status = res.data
|
||||
configurationToggling.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
// Configuration Summary =====================================
|
||||
const configurationSummary = computed(() => {
|
||||
return {
|
||||
connectedPeers: configurationPeers.value.filter(x => x.status === "running").length,
|
||||
totalUsage: configurationPeers.value.length > 0 ?
|
||||
configurationPeers.value.filter(x => !x.restricted)
|
||||
.map(x => x.total_data + x.cumu_data).reduce((a, b) => a + b, 0).toFixed(4) : 0,
|
||||
totalReceive: configurationPeers.value.length > 0 ?
|
||||
configurationPeers.value.filter(x => !x.restricted)
|
||||
.map(x => x.total_receive + x.cumu_receive).reduce((a, b) => a + b, 0).toFixed(4) : 0,
|
||||
totalSent: configurationPeers.value.length > 0 ?
|
||||
configurationPeers.value.filter(x => !x.restricted)
|
||||
.map(x => x.total_sent + x.cumu_sent).reduce((a, b) => a + b, 0).toFixed(4) : 0
|
||||
}
|
||||
})
|
||||
|
||||
const showPeersCount = ref(10)
|
||||
const showPeersThreshold = 20;
|
||||
const hiddenPeers = computed(() => {
|
||||
return wireguardConfigurationStore.Filter.HiddenTags.map(tag => {
|
||||
return configurationInfo.value.Info.PeerGroups[tag].Peers
|
||||
}).flat()
|
||||
})
|
||||
const taggedPeers = computed(() => {
|
||||
return Object.values(configurationInfo.value.Info.PeerGroups).map(x => x.Peers).flat()
|
||||
})
|
||||
|
||||
const firstAllowedIPCount = (allowed_ip) => {
|
||||
try{
|
||||
return parseCidr(allowed_ip.replace(" ", "").split(",")[0]).start
|
||||
}catch (e){
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
const searchPeers = computed(() => {
|
||||
const result = wireguardConfigurationStore.searchString ?
|
||||
configurationPeers.value.filter(x => {
|
||||
return (x.name.includes(wireguardConfigurationStore.searchString) ||
|
||||
x.id.includes(wireguardConfigurationStore.searchString) ||
|
||||
x.allowed_ip.includes(wireguardConfigurationStore.searchString))
|
||||
&& !hiddenPeers.value.includes(x.id)
|
||||
&& (
|
||||
wireguardConfigurationStore.Filter.ShowAllPeersWhenHiddenTags || (!wireguardConfigurationStore.Filter.ShowAllPeersWhenHiddenTags && taggedPeers.value.includes(x.id))
|
||||
)
|
||||
}) : configurationPeers.value.filter(x => !hiddenPeers.value.includes(x.id) && (
|
||||
wireguardConfigurationStore.Filter.ShowAllPeersWhenHiddenTags || (!wireguardConfigurationStore.Filter.ShowAllPeersWhenHiddenTags && taggedPeers.value.includes(x.id))
|
||||
));
|
||||
|
||||
if (dashboardStore.Configuration.Server.dashboard_sort === "restricted"){
|
||||
return result.sort((a, b) => {
|
||||
if ( a[dashboardStore.Configuration.Server.dashboard_sort]
|
||||
< b[dashboardStore.Configuration.Server.dashboard_sort] ){
|
||||
return 1;
|
||||
}
|
||||
if ( a[dashboardStore.Configuration.Server.dashboard_sort]
|
||||
> b[dashboardStore.Configuration.Server.dashboard_sort]){
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}).slice(0, showPeersCount.value);
|
||||
}
|
||||
|
||||
let re = []
|
||||
|
||||
if (dashboardStore.Configuration.Server.dashboard_sort === 'allowed_ip'){
|
||||
re = result.sort((a, b) => {
|
||||
if ( firstAllowedIPCount(a[dashboardStore.Configuration.Server.dashboard_sort])
|
||||
< firstAllowedIPCount(b[dashboardStore.Configuration.Server.dashboard_sort]) ){
|
||||
return -1;
|
||||
}
|
||||
if ( firstAllowedIPCount(a[dashboardStore.Configuration.Server.dashboard_sort])
|
||||
> firstAllowedIPCount(b[dashboardStore.Configuration.Server.dashboard_sort])){
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}).slice(0, showPeersCount.value)
|
||||
}else{
|
||||
re = result.sort((a, b) => {
|
||||
if ( a[dashboardStore.Configuration.Server.dashboard_sort]
|
||||
< b[dashboardStore.Configuration.Server.dashboard_sort] ){
|
||||
return -1;
|
||||
}
|
||||
if ( a[dashboardStore.Configuration.Server.dashboard_sort]
|
||||
> b[dashboardStore.Configuration.Server.dashboard_sort]){
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}).slice(0, showPeersCount.value)
|
||||
}
|
||||
|
||||
|
||||
return re
|
||||
})
|
||||
|
||||
watch(() => route.query.id, (newValue) => {
|
||||
if (newValue){
|
||||
wireguardConfigurationStore.searchString = newValue
|
||||
}else{
|
||||
wireguardConfigurationStore.searchString = undefined
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-fluid" >
|
||||
<div class="d-flex align-items-sm-start flex-column flex-sm-row gap-3">
|
||||
<div>
|
||||
<div class="text-muted d-flex align-items-center gap-2">
|
||||
<h5 class="mb-0">
|
||||
<ProtocolBadge :protocol="configurationInfo.Protocol"></ProtocolBadge>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<h1 class="mb-0 display-4"><samp>{{configurationInfo.Name}}</samp></h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-sm-auto d-flex gap-2 flex-column">
|
||||
<div class="card rounded-3 bg-transparent ">
|
||||
<div class="card-body py-2 d-flex align-items-center">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Status"></LocaleText>
|
||||
</small>
|
||||
<div class="dot ms-2" :class="{active: configurationInfo.Status}"></div>
|
||||
<div class="form-check form-switch mb-0 ms-auto pe-0 me-0">
|
||||
<label class="form-check-label" style="cursor: pointer" :for="'switch' + configurationInfo.id">
|
||||
<LocaleText t="On" v-if="configurationInfo.Status && !configurationToggling"></LocaleText>
|
||||
<LocaleText t="Off" v-else-if="!configurationInfo.Status && !configurationToggling"></LocaleText>
|
||||
<span v-if="configurationToggling"
|
||||
class="spinner-border spinner-border-sm ms-2" aria-hidden="true">
|
||||
</span>
|
||||
</label>
|
||||
<input class="form-check-input"
|
||||
style="cursor: pointer"
|
||||
:disabled="configurationToggling"
|
||||
type="checkbox" role="switch" :id="'switch' + configurationInfo.id"
|
||||
@change="toggleConfiguration()"
|
||||
v-model="configurationInfo.Status">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a
|
||||
role="button"
|
||||
@click="configurationModals.peerNew.modalOpen = true"
|
||||
class="titleBtn py-2 text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle ">
|
||||
<i class="bi bi-plus-circle me-2"></i>
|
||||
<LocaleText t="Peer"></LocaleText>
|
||||
</a>
|
||||
<button class="titleBtn py-2 text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle "
|
||||
@click="configurationModals.editConfiguration.modalOpen = true"
|
||||
type="button" aria-expanded="false">
|
||||
<i class="bi bi-gear-fill me-2"></i>
|
||||
<LocaleText t="Configuration Settings"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<ConfigurationDescription :configuration="configurationInfo"></ConfigurationDescription>
|
||||
<div class="row mt-3 gy-2 gx-2 mb-2">
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body py-2 d-flex flex-column justify-content-center">
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Address"></LocaleText>
|
||||
</small></p>
|
||||
{{configurationInfo.Address}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body py-2 d-flex flex-column justify-content-center">
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Listen Port"></LocaleText>
|
||||
</small></p>
|
||||
{{configurationInfo.ListenPort}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="word-break: break-all" class="col-12 col-lg-6">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body py-2 d-flex flex-column justify-content-center">
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Public Key"></LocaleText>
|
||||
</small></p>
|
||||
<samp>{{configurationInfo.PublicKey}}</samp>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gx-2 gy-2 mb-2">
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Connected Peers"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4">
|
||||
{{configurationSummary.connectedPeers}} / {{configurationPeers.length}}
|
||||
</strong>
|
||||
</div>
|
||||
<i class="bi bi-ethernet ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Total Usage"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4">{{configurationSummary.totalUsage}} GB</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-down-up ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Total Received"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4 text-primary">{{configurationSummary.totalReceive}} GB</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-down ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="card rounded-3 bg-transparent h-100">
|
||||
<div class="card-body d-flex">
|
||||
<div>
|
||||
<p class="mb-0 text-muted"><small>
|
||||
<LocaleText t="Total Sent"></LocaleText>
|
||||
</small></p>
|
||||
<strong class="h4 text-success">{{configurationSummary.totalSent}} GB</strong>
|
||||
</div>
|
||||
<i class="bi bi-arrow-up ms-auto h2 text-muted"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PeerDataUsageCharts
|
||||
:configurationPeers="configurationPeers"
|
||||
:configurationInfo="configurationInfo"
|
||||
></PeerDataUsageCharts>
|
||||
<hr>
|
||||
<div style="margin-bottom: 10rem">
|
||||
<PeerSearch
|
||||
v-if="configurationPeers.length > 0"
|
||||
@search="peerSearchBar = !peerSearchBar"
|
||||
@jobsAll="configurationModals.peerScheduleJobsAll.modalOpen = true"
|
||||
@jobLogs="configurationModals.peerScheduleJobsLogs.modalOpen = true"
|
||||
@editConfiguration="configurationModals.editConfiguration.modalOpen = true"
|
||||
@selectPeers="configurationModals.selectPeers.modalOpen = true"
|
||||
@backupRestore="configurationModals.backupRestore.modalOpen = true"
|
||||
@deleteConfiguration="configurationModals.deleteConfiguration.modalOpen = true"
|
||||
:configuration="configurationInfo">
|
||||
</PeerSearch>
|
||||
<TransitionGroup name="peerList" tag="div" class="row gx-2 gy-2 z-0 position-relative">
|
||||
<div class="col-12"
|
||||
:class="{'col-lg-6 col-xl-4': dashboardStore.Configuration.Server.dashboard_peer_list_display === 'grid'}"
|
||||
:key="peer.id"
|
||||
v-for="(peer, order) in searchPeers">
|
||||
<Peer :Peer="peer"
|
||||
:searchPeersLength="searchPeers.length"
|
||||
:order="order"
|
||||
:ConfigurationInfo="configurationInfo"
|
||||
@details="configurationModals.peerDetails.modalOpen = true; configurationModalSelectedPeer = peer"
|
||||
@share="configurationModals.peerShare.modalOpen = true; configurationModalSelectedPeer = peer"
|
||||
@refresh="fetchPeerList()"
|
||||
|
||||
@jobs="configurationModals.peerScheduleJobs.modalOpen = true; configurationModalSelectedPeer = peer"
|
||||
@setting="configurationModals.peerSetting.modalOpen = true; configurationModalSelectedPeer = peer"
|
||||
@qrcode="configurationModalSelectedPeer = peer; configurationModals.peerQRCode.modalOpen = true;"
|
||||
@configurationFile="configurationModalSelectedPeer = peer; configurationModals.peerConfigurationFile.modalOpen = true;"
|
||||
@assign="configurationModalSelectedPeer = peer; configurationModals.assignPeer.modalOpen = true;"
|
||||
></Peer>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
|
||||
</div>
|
||||
<Transition name="slide-fade">
|
||||
<PeerSearchBar
|
||||
v-if="peerSearchBar"
|
||||
:ConfigurationInfo="configurationInfo"
|
||||
@close="peerSearchBar = false"></PeerSearchBar>
|
||||
</Transition>
|
||||
<PeerListModals
|
||||
:configurationModals="configurationModals"
|
||||
:configurationModalSelectedPeer="configurationModalSelectedPeer"
|
||||
@refresh="fetchPeerList()"
|
||||
></PeerListModals>
|
||||
<TransitionGroup name="zoom">
|
||||
<Suspense key="PeerAddModal">
|
||||
<PeerAddModal
|
||||
v-if="configurationModals.peerNew.modalOpen"
|
||||
@close="configurationModals.peerNew.modalOpen = false"
|
||||
@addedPeers="configurationModals.peerNew.modalOpen = false; fetchPeerList()"
|
||||
></PeerAddModal>
|
||||
</Suspense>
|
||||
<PeerJobsAllModal
|
||||
key="PeerJobsAllModal"
|
||||
v-if="configurationModals.peerScheduleJobsAll.modalOpen"
|
||||
@refresh="fetchPeerList()"
|
||||
@allLogs="configurationModals.peerScheduleJobsLogs.modalOpen = true"
|
||||
@close="configurationModals.peerScheduleJobsAll.modalOpen = false"
|
||||
:configurationPeers="configurationPeers"
|
||||
>
|
||||
</PeerJobsAllModal>
|
||||
<PeerJobsLogsModal
|
||||
key="PeerJobsLogsModal"
|
||||
v-if="configurationModals.peerScheduleJobsLogs.modalOpen"
|
||||
@close="configurationModals.peerScheduleJobsLogs.modalOpen = false"
|
||||
:configurationInfo="configurationInfo">
|
||||
</PeerJobsLogsModal>
|
||||
<EditConfigurationModal
|
||||
key="EditConfigurationModal"
|
||||
@editRaw="configurationModals.editRawConfigurationFile.modalOpen = true"
|
||||
@close="configurationModals.editConfiguration.modalOpen = false"
|
||||
@dataChanged="(d) => configurationInfo = d"
|
||||
@refresh="fetchPeerList()"
|
||||
@backupRestore="configurationModals.backupRestore.modalOpen = true"
|
||||
@deleteConfiguration="configurationModals.deleteConfiguration.modalOpen = true"
|
||||
:configurationInfo="configurationInfo"
|
||||
v-if="configurationModals.editConfiguration.modalOpen">
|
||||
</EditConfigurationModal>
|
||||
<SelectPeersModal
|
||||
@refresh="fetchPeerList()"
|
||||
v-if="configurationModals.selectPeers.modalOpen"
|
||||
:configurationPeers="configurationPeers"
|
||||
@close="configurationModals.selectPeers.modalOpen = false"
|
||||
></SelectPeersModal>
|
||||
<PeerDetailsModal
|
||||
key="PeerDetailsModal"
|
||||
v-if="configurationModals.peerDetails.modalOpen"
|
||||
:selectedPeer="searchPeers.find(x => x.id === configurationModalSelectedPeer.id)"
|
||||
@close="configurationModals.peerDetails.modalOpen = false"
|
||||
>
|
||||
</PeerDetailsModal>
|
||||
</TransitionGroup>
|
||||
<PeerIntersectionObserver
|
||||
:showPeersCount="showPeersCount"
|
||||
:peerListLength="searchPeers.length"
|
||||
@loadMore="showPeersCount += showPeersThreshold"></PeerIntersectionObserver>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.peerNav .nav-link{
|
||||
&.active{
|
||||
background-color: #efefef;
|
||||
}
|
||||
}
|
||||
|
||||
th, td{
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 576px) {
|
||||
.titleBtn{
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,289 @@
|
||||
<script setup>
|
||||
import {computed, defineComponent, onBeforeUnmount, onMounted, reactive, ref, useTemplateRef, watch} from "vue";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import { Line, Bar } from 'vue-chartjs'
|
||||
import {
|
||||
Chart,
|
||||
LineElement,
|
||||
BarElement,
|
||||
BarController,
|
||||
LineController,
|
||||
LinearScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
CategoryScale,
|
||||
PointElement,
|
||||
Filler
|
||||
} from 'chart.js';
|
||||
Chart.register(
|
||||
LineElement,
|
||||
BarElement,
|
||||
BarController,
|
||||
LineController,
|
||||
LinearScale,
|
||||
Legend,
|
||||
Title,
|
||||
Tooltip,
|
||||
CategoryScale,
|
||||
PointElement,
|
||||
Filler
|
||||
);
|
||||
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import dayjs from "dayjs";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
const props = defineProps({
|
||||
configurationPeers: Array,
|
||||
configurationInfo: Object
|
||||
})
|
||||
|
||||
const historySentData = ref({
|
||||
timestamp: [],
|
||||
data: []
|
||||
})
|
||||
|
||||
const historyReceivedData = ref({
|
||||
timestamp: [],
|
||||
data: []
|
||||
})
|
||||
const route = useRoute()
|
||||
const dashboardStore = DashboardConfigurationStore()
|
||||
const fetchRealtimeTrafficInterval = ref(undefined)
|
||||
const fetchRealtimeTraffic = async () => {
|
||||
await fetchGet("/api/getWireguardConfigurationRealtimeTraffic", {
|
||||
configurationName: route.params.id
|
||||
}, (res) => {
|
||||
let timestamp = dayjs().format("hh:mm:ss A")
|
||||
if (res.data.sent !== 0 && res.data.recv !== 0){
|
||||
historySentData.value.timestamp.push(timestamp)
|
||||
historySentData.value.data.push(res.data.sent)
|
||||
|
||||
historyReceivedData.value.timestamp.push(timestamp)
|
||||
historyReceivedData.value.data.push(res.data.recv)
|
||||
}else{
|
||||
if (historySentData.value.data.length > 0 && historyReceivedData.value.data.length > 0){
|
||||
historySentData.value.timestamp.push(timestamp)
|
||||
historySentData.value.data.push(res.data.sent)
|
||||
|
||||
historyReceivedData.value.timestamp.push(timestamp)
|
||||
historyReceivedData.value.data.push(res.data.recv)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
const toggleFetchRealtimeTraffic = () => {
|
||||
clearInterval(fetchRealtimeTrafficInterval.value)
|
||||
fetchRealtimeTrafficInterval.value = undefined;
|
||||
if (props.configurationInfo.Status){
|
||||
fetchRealtimeTrafficInterval.value = setInterval(() => {
|
||||
fetchRealtimeTraffic()
|
||||
}, parseInt(dashboardStore.Configuration.Server.dashboard_refresh_interval))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
toggleFetchRealtimeTraffic()
|
||||
})
|
||||
|
||||
watch(() => props.configurationInfo.Status, () => {
|
||||
toggleFetchRealtimeTraffic()
|
||||
})
|
||||
|
||||
watch(() => dashboardStore.Configuration.Server.dashboard_refresh_interval, () => {
|
||||
toggleFetchRealtimeTraffic()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(fetchRealtimeTrafficInterval.value)
|
||||
fetchRealtimeTrafficInterval.value = undefined;
|
||||
})
|
||||
const peersDataUsageChartData = computed(() => {
|
||||
let data = props.configurationPeers.filter(x => (x.cumu_data + x.total_data) > 0)
|
||||
|
||||
return {
|
||||
labels: data.map(x => {
|
||||
if (x.name) return x.name
|
||||
return `Untitled Peer - ${x.id}`
|
||||
}),
|
||||
datasets: [{
|
||||
label: 'Total Data Usage',
|
||||
data: data.map(x => x.cumu_data + x.total_data),
|
||||
backgroundColor: data.map(x => `#ffc107`),
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (tooltipItem) => {
|
||||
return `${tooltipItem.formattedValue} GB`
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
const peersRealtimeSentData = computed(() => {
|
||||
return {
|
||||
labels: [...historySentData.value.timestamp],
|
||||
datasets: [
|
||||
{
|
||||
label: GetLocale('Data Sent'),
|
||||
data: [...historySentData.value.data],
|
||||
fill: 'start',
|
||||
borderColor: '#198754',
|
||||
backgroundColor: '#19875490',
|
||||
tension: 0,
|
||||
pointRadius: 2,
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
const peersRealtimeReceivedData = computed(() => {
|
||||
return {
|
||||
labels: [...historyReceivedData.value.timestamp],
|
||||
datasets: [
|
||||
{
|
||||
label: GetLocale('Data Received'),
|
||||
data: [...historyReceivedData.value.data],
|
||||
fill: 'start',
|
||||
borderColor: '#0d6efd',
|
||||
backgroundColor: '#0d6efd90',
|
||||
tension: 0,
|
||||
pointRadius: 2,
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const peersDataUsageChartOption = computed(() => {
|
||||
return {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
},
|
||||
y:{
|
||||
ticks: {
|
||||
callback: (val, index) => {
|
||||
return `${Math.round((val + Number.EPSILON) * 1000) / 1000} GB`
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
const realtimePeersChartOption = computed(() => {
|
||||
return {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (tooltipItem) => {
|
||||
return `${tooltipItem.formattedValue} MB/s`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
grid: {
|
||||
display: true
|
||||
},
|
||||
},
|
||||
y:{
|
||||
ticks: {
|
||||
callback: (val, index) => {
|
||||
return `${Math.round((val + Number.EPSILON) * 1000) / 1000} MB/s`
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
display: true
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row gx-2 gy-2 mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card rounded-3 bg-transparent " style="height: 270px">
|
||||
<div class="card-header bg-transparent border-0">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Peers Data Usage"></LocaleText>
|
||||
</small></div>
|
||||
<div class="card-body pt-1">
|
||||
<Bar
|
||||
:data="peersDataUsageChartData"
|
||||
:options="peersDataUsageChartOption"
|
||||
style="width: 100%; height: 200px; max-height: 200px"></Bar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm col-lg-6">
|
||||
<div class="card rounded-3 bg-transparent " style="height: 270px">
|
||||
<div class="card-header bg-transparent border-0 d-flex align-items-center">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Real Time Received Data Usage"></LocaleText>
|
||||
</small>
|
||||
<small class="text-primary fw-bold ms-auto" v-if="historyReceivedData.data.length > 0">
|
||||
{{historyReceivedData.data[historyReceivedData.data.length - 1]}} MB/s
|
||||
</small>
|
||||
</div>
|
||||
<div class="card-body pt-1">
|
||||
<Line
|
||||
:options="realtimePeersChartOption"
|
||||
:data="peersRealtimeReceivedData"
|
||||
style="width: 100%; height: 200px; max-height: 200px"
|
||||
></Line>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm col-lg-6">
|
||||
<div class="card rounded-3 bg-transparent " style="height: 270px">
|
||||
<div class="card-header bg-transparent border-0 d-flex align-items-center">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Real Time Sent Data Usage"></LocaleText>
|
||||
</small>
|
||||
<small class="text-success fw-bold ms-auto" v-if="historySentData.data.length > 0">
|
||||
{{historySentData.data[historySentData.data.length - 1]}} MB/s
|
||||
</small>
|
||||
</div>
|
||||
<div class="card-body pt-1">
|
||||
<Line
|
||||
:options="realtimePeersChartOption"
|
||||
:data="peersRealtimeSentData"
|
||||
style="width: 100%; height: 200px; max-height: 200px"
|
||||
></Line>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
import {defineAsyncComponent} from "vue";
|
||||
const props = defineProps({
|
||||
configurationModals: Object,
|
||||
configurationModalSelectedPeer: Object
|
||||
})
|
||||
const emits = defineEmits(["refresh"])
|
||||
|
||||
const PeerAssignModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerAssignModal.vue"))
|
||||
const PeerShareLinkModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerShareLinkModal.vue"))
|
||||
const PeerJobsModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerJobs.vue"))
|
||||
const PeerQRCodeModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerQRCode.vue"))
|
||||
const PeerConfigurationFileModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerConfigurationFile.vue"))
|
||||
const PeerSettingsModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerSettings.vue"))
|
||||
const PeerDetailsModal = defineAsyncComponent(() => import("@/components/configurationComponents/peerDetailsModal.vue"))
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TransitionGroup name="zoom">
|
||||
<PeerSettingsModal
|
||||
v-if="configurationModals.peerSetting.modalOpen"
|
||||
key="PeerSettingsModal"
|
||||
:selectedPeer="configurationModalSelectedPeer"
|
||||
@refresh="emits('refresh')"
|
||||
@close="configurationModals.peerSetting.modalOpen = false">
|
||||
</PeerSettingsModal>
|
||||
<PeerQRCodeModal
|
||||
key="PeerQRCodeModal"
|
||||
v-if="configurationModals.peerQRCode.modalOpen"
|
||||
:selectedPeer="configurationModalSelectedPeer"
|
||||
@close="configurationModals.peerQRCode.modalOpen = false">
|
||||
</PeerQRCodeModal>
|
||||
<PeerJobsModal
|
||||
key="PeerJobsModal"
|
||||
@refresh="emits('refresh')"
|
||||
v-if="configurationModals.peerScheduleJobs.modalOpen"
|
||||
:selectedPeer="configurationModalSelectedPeer"
|
||||
@close="configurationModals.peerScheduleJobs.modalOpen = false">
|
||||
</PeerJobsModal>
|
||||
<PeerShareLinkModal
|
||||
key="PeerShareLinkModal"
|
||||
v-if="configurationModals.peerShare.modalOpen"
|
||||
@close="configurationModals.peerShare.modalOpen = false;"
|
||||
:selectedPeer="configurationModalSelectedPeer">
|
||||
</PeerShareLinkModal>
|
||||
<PeerConfigurationFileModal
|
||||
key="PeerConfigurationFileModal"
|
||||
@close="configurationModals.peerConfigurationFile.modalOpen = false"
|
||||
v-if="configurationModals.peerConfigurationFile.modalOpen"
|
||||
:selectedPeer="configurationModalSelectedPeer"
|
||||
></PeerConfigurationFileModal>
|
||||
<PeerAssignModal
|
||||
key="PeerAssignModal"
|
||||
:selectedPeer="configurationModalSelectedPeer"
|
||||
@close="configurationModals.assignPeer.modalOpen = false"
|
||||
v-if="configurationModals.assignPeer.modalOpen"></PeerAssignModal>
|
||||
|
||||
</TransitionGroup>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script>
|
||||
import QRCode from "qrcode";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
export default {
|
||||
name: "peerQRCode",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
selectedPeer: Object
|
||||
},
|
||||
setup(){
|
||||
const dashboardStore = DashboardConfigurationStore();
|
||||
return {dashboardStore}
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
fetchGet("/api/downloadPeer/"+this.$route.params.id, {
|
||||
id: this.selectedPeer.id
|
||||
}, (res) => {
|
||||
this.loading = false;
|
||||
if (res.status){
|
||||
let data = ""
|
||||
if (this.selectedPeer.configuration.Protocol === "awg"){
|
||||
let awgQRCodeObject = {
|
||||
containers: [
|
||||
{
|
||||
awg: {
|
||||
isThirdPartyConfig: true,
|
||||
last_config: res.data.file,
|
||||
port: this.selectedPeer.configuration.ListenPort,
|
||||
transport_proto: "udp"
|
||||
},
|
||||
container: "amnezia-awg",
|
||||
}
|
||||
],
|
||||
defaultContainer: "amnezia-awg",
|
||||
description: this.selectedPeer.name,
|
||||
hostName: this.dashboardStore.Configuration.Peers.remote_endpoint
|
||||
}
|
||||
QRCode.toCanvas(
|
||||
document.querySelector("#awg_vpn_qrcode"), btoa(JSON.stringify(awgQRCodeObject)), (error) => {
|
||||
if (error) console.error(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
data = res.data.file
|
||||
QRCode.toCanvas(document.querySelector("#qrcode"), data, (error) => {
|
||||
if (error) console.error(error)
|
||||
})
|
||||
}else{
|
||||
this.dashboardStore.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0">
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal justify-content-center">
|
||||
<div class="card rounded-3 shadow">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-0">
|
||||
<h4 class="mb-0">
|
||||
<LocaleText t="QR Code"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex gap-2 flex-column">
|
||||
<div class="d-flex flex-column gap-2 align-items-center">
|
||||
<canvas id="qrcode"
|
||||
style="width: 200px !important; height: 200px !important;"
|
||||
class="rounded-3 shadow animate__animated animate__fadeIn animate__faster qrcode"
|
||||
:class="{'d-none': loading}"></canvas>
|
||||
<LocaleText t="Scan with WireGuard App" class="text-muted" v-if='this.selectedPeer.configuration.Protocol === "wg"'></LocaleText>
|
||||
<LocaleText t="Scan with AmneziaWG App" class="text-muted" v-if='this.selectedPeer.configuration.Protocol === "awg"'></LocaleText>
|
||||
</div>
|
||||
<div v-if='this.selectedPeer.configuration.Protocol === "awg"' class="d-flex flex-column gap-2 align-items-center">
|
||||
<canvas id="awg_vpn_qrcode" class="rounded-3 shadow animate__animated animate__fadeIn animate__faster qrcode"
|
||||
:class="{'d-none': loading}"></canvas>
|
||||
<LocaleText t="Scan with AmneziaVPN App" class="text-muted"></LocaleText>
|
||||
</div>
|
||||
<div class="spinner-border m-auto" role="status" v-if="loading">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (max-width: 768px) {
|
||||
.qrcode{
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode{
|
||||
width: 200px !important;
|
||||
height: 200px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import {computed, ref, useTemplateRef} from "vue";
|
||||
import PeerSettingsDropdown from "@/components/configurationComponents/peerSettingsDropdown.vue";
|
||||
import {onClickOutside} from "@vueuse/core";
|
||||
|
||||
const props = defineProps(['Peer'])
|
||||
const subMenuOpened = ref(false)
|
||||
const getLatestHandshake = computed(() => {
|
||||
if (props.Peer.latest_handshake.includes(",")){
|
||||
return props.Peer.latest_handshake.split(",")[0]
|
||||
}
|
||||
return props.Peer.latest_handshake;
|
||||
})
|
||||
|
||||
const target = useTemplateRef('target');
|
||||
onClickOutside(target, event => {
|
||||
subMenuOpened.value = false;
|
||||
});
|
||||
|
||||
const emit = defineEmits(['qrcode', 'configurationFile', 'setting', 'jobs', 'refresh', 'share'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr>
|
||||
<td>
|
||||
<small>{{Peer.name ? Peer.name : 'Untitled Peer'}}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>{{Peer.id}}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>
|
||||
{{Peer.allowed_ip}}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-primary">
|
||||
{{(Peer.cumu_receive + Peer.total_receive).toFixed(4)}} GB
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-success">
|
||||
{{(Peer.cumu_sent + Peer.total_sent).toFixed(4)}} GB
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-secondary" v-if="Peer.latest_handshake !== 'No Handshake'">
|
||||
<i class="bi bi-arrows-angle-contract"></i>
|
||||
{{getLatestHandshake}} ago
|
||||
</small>
|
||||
<small v-else>N/A</small>
|
||||
</td>
|
||||
<td>
|
||||
<a role="button" class="text-body"
|
||||
@click="subMenuOpened = true">
|
||||
<h5 class="mb-0"><i class="bi bi-three-dots"></i></h5>
|
||||
</a>
|
||||
<Transition name="slide-fade">
|
||||
<PeerSettingsDropdown
|
||||
@qrcode="(file) => emit('qrcode', file)"
|
||||
@configurationFile="(file) => emit('configurationFile', file)"
|
||||
@setting="emit('setting')"
|
||||
@jobs="emit('jobs')"
|
||||
@refresh="emit('refresh')"
|
||||
@share="emit('share')"
|
||||
:Peer="Peer"
|
||||
v-if="subMenuOpened"
|
||||
ref="target"
|
||||
></PeerSettingsDropdown>
|
||||
</Transition>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
td{
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script>
|
||||
|
||||
|
||||
export default {
|
||||
name: "scheduleDropdown",
|
||||
props: {
|
||||
options: Array,
|
||||
data: String,
|
||||
edit: false
|
||||
},
|
||||
setup(props) {
|
||||
if (props.data === undefined){
|
||||
this.$emit('update', this.options[0].value)
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
currentSelection(){
|
||||
return this.options.find(x => x.value === this.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown scheduleDropdown">
|
||||
<button class="btn btn-sm btn-outline-primary rounded-3"
|
||||
:class="{'disabled border-transparent': !edit}" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<samp>{{this.currentSelection.display}}</samp>
|
||||
</button>
|
||||
<ul class="dropdown-menu rounded-3 shadow" style="font-size: 0.875rem;">
|
||||
<li v-for="x in this.options" v-if="edit">
|
||||
<a class="dropdown-item d-flex align-items-center" role="button" @click="$emit('update', x.value)">
|
||||
<samp class="pe-5">{{x.display}}</samp>
|
||||
<i class="bi bi-check ms-auto" v-if="x.value === this.currentSelection.value"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.btn.disabled{
|
||||
opacity: 1;
|
||||
background-color: rgba(13, 110, 253, 0.09);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,219 @@
|
||||
<script>
|
||||
import ScheduleDropdown from "@/components/configurationComponents/peerScheduleJobsComponents/scheduleDropdown.vue";
|
||||
import {ref} from "vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
import { VueDatePicker } from "@vuepic/vue-datepicker";
|
||||
import dayjs from "dayjs";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "schedulePeerJob",
|
||||
components: {LocaleText, VueDatePicker, ScheduleDropdown},
|
||||
props: {
|
||||
dropdowns: Array[Object],
|
||||
pjob: Object,
|
||||
viewOnly: false
|
||||
},
|
||||
setup(props){
|
||||
const job = ref({})
|
||||
const edit = ref(false)
|
||||
const newJob = ref(false)
|
||||
job.value = JSON.parse(JSON.stringify(props.pjob))
|
||||
if (!job.value.CreationDate){
|
||||
edit.value = true
|
||||
newJob.value = true
|
||||
}
|
||||
const store = DashboardConfigurationStore()
|
||||
return {job, edit, newJob, store}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
inputType: undefined,
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
pjob: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(newValue){
|
||||
if (!this.edit){
|
||||
this.job = JSON.parse(JSON.stringify(newValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save(){
|
||||
if (this.job.Field && this.job.Operator && this.job.Action && this.job.Value){
|
||||
fetchPost(`/api/savePeerScheduleJob`, {
|
||||
Job: this.job
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.edit = false;
|
||||
this.store.newMessage("Server", "Peer job saved", "success")
|
||||
console.log(res.data)
|
||||
this.$emit("refresh", res.data[0])
|
||||
this.newJob = false;
|
||||
}else{
|
||||
this.store.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
})
|
||||
}else{
|
||||
this.alert();
|
||||
}
|
||||
},
|
||||
alert(){
|
||||
let animation = "animate__flash";
|
||||
let dropdowns = this.$el.querySelectorAll(".scheduleDropdown");
|
||||
let inputs = this.$el.querySelectorAll("input");
|
||||
dropdowns.forEach(x => x.classList.add("animate__animated", animation))
|
||||
inputs.forEach(x => x.classList.add("animate__animated", animation))
|
||||
setTimeout(() => {
|
||||
dropdowns.forEach(x => x.classList.remove("animate__animated", animation))
|
||||
inputs.forEach(x => x.classList.remove("animate__animated", animation))
|
||||
}, 2000)
|
||||
},
|
||||
reset(){
|
||||
if(this.job.CreationDate){
|
||||
this.job = JSON.parse(JSON.stringify(this.pjob));
|
||||
this.edit = false;
|
||||
}else{
|
||||
this.$emit('delete')
|
||||
}
|
||||
},
|
||||
delete(){
|
||||
if(this.job.CreationDate){
|
||||
fetchPost(`/api/deletePeerScheduleJob`, {
|
||||
Job: this.job
|
||||
}, (res) => {
|
||||
if (!res.status){
|
||||
this.store.newMessage("Server", res.message, "danger")
|
||||
this.$emit('delete')
|
||||
}else{
|
||||
this.store.newMessage("Server", "Peer job deleted", "success")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
this.$emit('delete')
|
||||
},
|
||||
parseTime(modelData){
|
||||
if(modelData){
|
||||
this.job.Value = dayjs(modelData).format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card shadow-sm rounded-3 mb-2" :class="{'border-warning-subtle': this.newJob}">
|
||||
<div class="card-header bg-transparent text-muted border-0">
|
||||
<small class="d-flex" v-if="!this.newJob">
|
||||
<strong class="me-auto">
|
||||
<LocaleText t="Job ID"></LocaleText>
|
||||
</strong>
|
||||
<samp>{{this.job.JobID}}</samp>
|
||||
</small>
|
||||
<small v-else><span class="badge text-bg-warning">
|
||||
<LocaleText t="Unsaved Job"></LocaleText>
|
||||
</span></small>
|
||||
</div>
|
||||
<div class="card-body pt-1" style="font-family: var(--bs-font-monospace)">
|
||||
<div class="d-flex gap-2 align-items-center mb-2">
|
||||
<samp>
|
||||
<LocaleText t="if"></LocaleText>
|
||||
</samp>
|
||||
<ScheduleDropdown
|
||||
:edit="edit"
|
||||
:options="this.dropdowns.Field"
|
||||
:data="this.job.Field"
|
||||
@update="(value) => {this.job.Field = value}"
|
||||
></ScheduleDropdown>
|
||||
<samp>
|
||||
<LocaleText t="is"></LocaleText>
|
||||
</samp>
|
||||
<ScheduleDropdown
|
||||
:edit="edit"
|
||||
:options="this.dropdowns.Operator"
|
||||
:data="this.job.Operator"
|
||||
@update="(value) => this.job.Operator = value"
|
||||
></ScheduleDropdown>
|
||||
|
||||
<VueDatePicker
|
||||
:is24="true"
|
||||
:min-date="new Date()"
|
||||
:model-value="this.job.Value"
|
||||
@update:model-value="this.parseTime" time-picker-inline
|
||||
format="yyyy-MM-dd HH:mm:ss"
|
||||
preview-format="yyyy-MM-dd HH:mm:ss"
|
||||
:clearable="false"
|
||||
:disabled="!edit"
|
||||
v-if="this.job.Field === 'date'"
|
||||
:dark="this.store.Configuration.Server.dashboard_theme === 'dark'"
|
||||
/>
|
||||
|
||||
<input class="form-control form-control-sm form-control-dark rounded-3 flex-grow-1"
|
||||
:disabled="!edit"
|
||||
v-else
|
||||
v-model="this.job.Value"
|
||||
style="width: auto">
|
||||
<samp>
|
||||
{{this.dropdowns.Field.find(x => x.value === this.job.Field)?.unit}} {
|
||||
</samp>
|
||||
</div>
|
||||
<div class="px-5 d-flex gap-2 align-items-center">
|
||||
<samp><LocaleText t="then"></LocaleText></samp>
|
||||
<ScheduleDropdown
|
||||
:edit="edit"
|
||||
:options="this.dropdowns.Action"
|
||||
:data="this.job.Action"
|
||||
@update="(value) => this.job.Action = value"
|
||||
></ScheduleDropdown>
|
||||
</div>
|
||||
<div class="d-flex gap-3">
|
||||
<samp>}</samp>
|
||||
<div class="ms-auto d-flex gap-3" v-if="!this.edit">
|
||||
<a role="button"
|
||||
class="ms-auto text-decoration-none"
|
||||
@click="this.edit = true">[E] <LocaleText t="Edit"></LocaleText></a>
|
||||
<a role="button"
|
||||
@click="this.delete()"
|
||||
class=" text-danger text-decoration-none">[D] <LocaleText t="Delete"></LocaleText></a>
|
||||
</div>
|
||||
<div class="ms-auto d-flex gap-3" v-else>
|
||||
<a role="button"
|
||||
class="text-secondary text-decoration-none"
|
||||
@click="this.reset()">[C] <LocaleText t="Cancel"></LocaleText></a>
|
||||
<a role="button"
|
||||
class="text-primary ms-auto text-decoration-none"
|
||||
@click="this.save()">[S] <LocaleText t="Save"></LocaleText></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
*{
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
input{
|
||||
padding: 0.1rem 0.4rem;
|
||||
}
|
||||
input:disabled{
|
||||
border-color: transparent;
|
||||
background-color: rgba(13, 110, 253, 0.09);
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.dp__main{
|
||||
width: auto;
|
||||
flex-grow: 1;
|
||||
--dp-input-padding: 2.5px 30px 2.5px 12px;
|
||||
--dp-border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,245 @@
|
||||
<script>
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
import PeerTag from "@/components/configurationComponents/peerTag.vue";
|
||||
|
||||
|
||||
export default {
|
||||
name: "peerSearch",
|
||||
components: {PeerTag, LocaleText},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
const wireguardConfigurationStore = WireguardConfigurationsStore()
|
||||
return {store, wireguardConfigurationStore}
|
||||
},
|
||||
props: {
|
||||
configuration: Object, displayTags: Array
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
sort: {
|
||||
status: GetLocale("Status"),
|
||||
name: GetLocale("Name"),
|
||||
allowed_ip: GetLocale("Allowed IPs"),
|
||||
restricted: GetLocale("Restricted")
|
||||
},
|
||||
interval: {
|
||||
'5000': GetLocale('5 Seconds'),
|
||||
'10000': GetLocale('10 Seconds'),
|
||||
'30000': GetLocale('30 Seconds'),
|
||||
'60000': GetLocale('1 Minutes')
|
||||
},
|
||||
display: {
|
||||
grid: GetLocale('Grid'),
|
||||
list: GetLocale('List')
|
||||
},
|
||||
searchString: "",
|
||||
searchStringTimeout: undefined,
|
||||
showDisplaySettings: false,
|
||||
showMoreSettings: false,
|
||||
tagManager: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateSort(sort){
|
||||
fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Server",
|
||||
key: "dashboard_sort",
|
||||
value: sort
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.store.getConfiguration();
|
||||
}
|
||||
})
|
||||
},
|
||||
updateRefreshInterval(refreshInterval){
|
||||
fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Server",
|
||||
key: "dashboard_refresh_interval",
|
||||
value: refreshInterval
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.store.getConfiguration();
|
||||
}
|
||||
})
|
||||
},
|
||||
updateDisplay(display){
|
||||
fetchPost("/api/updateDashboardConfigurationItem", {
|
||||
section: "Server",
|
||||
key: "dashboard_peer_list_display",
|
||||
value: display
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.store.getConfiguration();
|
||||
}
|
||||
})
|
||||
},
|
||||
downloadAllPeer(){
|
||||
fetchGet(`/api/downloadAllPeers/${this.configuration.Name}`, {}, (res) => {
|
||||
res.data.forEach(x => {
|
||||
x.fileName = x.fileName + '.conf'
|
||||
})
|
||||
window.wireguard.generateZipFiles(res, this.configuration.Name)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column gap-2 my-4">
|
||||
<div class="d-flex gap-2 peerSearchContainer">
|
||||
<div class="dropdown">
|
||||
<button
|
||||
data-bs-toggle="dropdown"
|
||||
class="btn w-100 btn-sm text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle position-relative">
|
||||
<i class="bi bi-sort-up me-2"></i>
|
||||
<LocaleText t="Sort By"></LocaleText>
|
||||
<span class="badge text-bg-primary ms-2">{{this.sort[store.Configuration.Server.dashboard_sort]}}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu rounded-3">
|
||||
<li v-for="(value, key) in this.sort" >
|
||||
<button class="dropdown-item d-flex align-items-center" @click="this.updateSort(key)">
|
||||
<small>
|
||||
{{ value }}
|
||||
</small>
|
||||
<small class="ms-auto">
|
||||
<i class="bi bi-check-circle-fill"
|
||||
v-if="store.Configuration.Server.dashboard_sort === key"></i>
|
||||
</small>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button
|
||||
data-bs-toggle="dropdown"
|
||||
class="btn btn-sm w-100 text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle position-relative">
|
||||
<i class="bi bi-arrow-repeat me-2"></i>
|
||||
<LocaleText t="Refresh Interval"></LocaleText>
|
||||
<span class="badge text-bg-primary ms-2">{{this.interval[store.Configuration.Server.dashboard_refresh_interval]}}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu rounded-3">
|
||||
<li v-for="(value, key) in this.interval" >
|
||||
<button class="dropdown-item d-flex align-items-center" @click="this.updateRefreshInterval(key)">
|
||||
<small>
|
||||
{{ value }}
|
||||
</small>
|
||||
<small class="ms-auto">
|
||||
<i class="bi bi-check-circle-fill"
|
||||
v-if="store.Configuration.Server.dashboard_refresh_interval === key"></i>
|
||||
</small>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button
|
||||
data-bs-toggle="dropdown"
|
||||
class="btn btn-sm w-100 text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle position-relative">
|
||||
<i class="bi me-2" :class="'bi-' + store.Configuration.Server.dashboard_peer_list_display"></i>
|
||||
<LocaleText t="Display"></LocaleText>
|
||||
<span class="badge text-bg-primary ms-2">{{this.display[store.Configuration.Server.dashboard_peer_list_display]}}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu rounded-3">
|
||||
<li v-for="(value, key) in this.display" >
|
||||
<button class="dropdown-item d-flex align-items-center" @click="this.updateDisplay(key)">
|
||||
<small>
|
||||
{{ value }}
|
||||
</small>
|
||||
<small class="ms-auto">
|
||||
<i class="bi bi-check-circle-fill"
|
||||
v-if="store.Configuration.Server.dashboard_peer_list_display === key"></i>
|
||||
</small>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="position-relative">
|
||||
<button
|
||||
@click="tagManager = !tagManager"
|
||||
class="btn btn-sm w-100 text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle position-relative">
|
||||
|
||||
<i class="bi me-2 bi-tag"></i>
|
||||
<LocaleText t="Tags"></LocaleText>
|
||||
|
||||
</button>
|
||||
<Transition name="slide-fade">
|
||||
<PeerTag
|
||||
@update="args => configuration.Info.PeerGroups = args"
|
||||
@close="this.tagManager = false"
|
||||
:configuration="configuration" v-if="this.tagManager"></PeerTag>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-sm text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle ms-lg-auto"
|
||||
@click="this.$emit('search')">
|
||||
<i class="bi bi-search me-2"></i>
|
||||
<LocaleText t="Search"></LocaleText>
|
||||
</button>
|
||||
<button class="btn btn-sm text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle"
|
||||
@click="this.downloadAllPeer()">
|
||||
<i class="bi bi-download me-2 me-lg-0 me-xl-2"></i>
|
||||
<LocaleText t="Download All" class="d-sm-block d-lg-none d-xl-block"></LocaleText>
|
||||
</button>
|
||||
<button class="btn btn-sm text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle "
|
||||
@click="this.$emit('selectPeers')">
|
||||
<i class="bi bi-check2-all me-2 me-lg-0 me-xl-2"></i>
|
||||
<LocaleText t="Select Peers" class="d-sm-block d-lg-none d-xl-block"></LocaleText>
|
||||
</button>
|
||||
<button class="btn btn-sm text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle "
|
||||
@click="this.$emit('jobsAll')"
|
||||
type="button" aria-expanded="false">
|
||||
<i class="bi bi-person-walking me-2 me-lg-0 me-xl-2"></i>
|
||||
<LocaleText t="Active Jobs" class="d-sm-block d-lg-none d-xl-block"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.animation__fadeInDropdown{
|
||||
animation-name: fadeInDropdown;
|
||||
animation-duration: 0.2s;
|
||||
animation-timing-function: cubic-bezier(0.82, 0.58, 0.17, 0.9);
|
||||
}
|
||||
|
||||
@keyframes fadeInDropdown{
|
||||
0%{
|
||||
opacity: 0;
|
||||
filter: blur(3px);
|
||||
transform: translateY(-60px);
|
||||
}
|
||||
100%{
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
}
|
||||
|
||||
.displayModal .dashboardModal{
|
||||
width: 400px !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.peerSearchContainer{
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.peerSettingContainer .dashboardModal{
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.peerSearchContainer > button, .peerSearchContainer .dropdown > button{
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup>
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
import {computed, onMounted, ref, useTemplateRef} from "vue";
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {onBeforeRouteUpdate, useRoute, useRouter} from "vue-router";
|
||||
|
||||
const searchBarPlaceholder = computed(() => {
|
||||
return GetLocale("Search Peers...")
|
||||
})
|
||||
let searchStringTimeout = undefined
|
||||
const wireguardConfigurationStore = WireguardConfigurationsStore()
|
||||
const searchString = ref(wireguardConfigurationStore.searchString)
|
||||
|
||||
const debounce = () => {
|
||||
if (!searchStringTimeout){
|
||||
searchStringTimeout = setTimeout(() => {
|
||||
wireguardConfigurationStore.searchString = searchString.value;
|
||||
}, 300)
|
||||
}else{
|
||||
clearTimeout(searchStringTimeout)
|
||||
searchStringTimeout = setTimeout(() => {
|
||||
wireguardConfigurationStore.searchString = searchString.value;
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
const emits = defineEmits(['close'])
|
||||
const input = useTemplateRef('searchBar')
|
||||
const props = defineProps(["ConfigurationInfo"])
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
if (route.query.peer){
|
||||
searchString.value = route.query.peer
|
||||
router.replace({ query: null })
|
||||
}
|
||||
|
||||
const show = ref(true)
|
||||
onMounted(() => {
|
||||
document.querySelector("#searchPeers").focus()
|
||||
})
|
||||
|
||||
onBeforeRouteUpdate(() => {
|
||||
show.value = false
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed-bottom w-100 bottom-0 z-2 p-3" style="z-index: 1;" v-if="show">
|
||||
<div class="d-flex flex-column searchPeersContainer ms-auto p-2 rounded-5"
|
||||
style="width: 300px;">
|
||||
<div class="rounded-5 border border-white p-2 d-flex align-items-center gap-1 w-100">
|
||||
<input
|
||||
ref="searchBar"
|
||||
|
||||
class="flex-grow-1 form-control form-control-sm rounded-5 bg-transparent border-0 border-secondary-subtle "
|
||||
:placeholder="searchBarPlaceholder"
|
||||
id="searchPeers"
|
||||
@keyup="debounce()"
|
||||
v-model="searchString">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.searchPeersContainer{
|
||||
backdrop-filter: blur(8px);
|
||||
width: 100%;
|
||||
background: linear-gradient(var(--degree), rgba(45, 173, 255, 0.4), rgba(255, 108, 109, 0.4), var(--brandColor2) 100%);
|
||||
}
|
||||
#searchPeers::placeholder{
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,274 @@
|
||||
<script>
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
export default {
|
||||
name: "peerSettings",
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
selectedPeer: Object
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
data: undefined,
|
||||
dataChanged: false,
|
||||
showKey: false,
|
||||
saving: false
|
||||
}
|
||||
},
|
||||
setup(){
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore();
|
||||
return {dashboardConfigurationStore}
|
||||
},
|
||||
methods: {
|
||||
reset(){
|
||||
if (this.selectedPeer){
|
||||
this.data = JSON.parse(JSON.stringify(this.selectedPeer))
|
||||
this.dataChanged = false;
|
||||
}
|
||||
},
|
||||
savePeer(){
|
||||
this.saving = true;
|
||||
fetchPost(`/api/updatePeerSettings/${this.$route.params.id}`, this.data, (res) => {
|
||||
this.saving = false;
|
||||
if (res.status){
|
||||
this.dashboardConfigurationStore.newMessage("Server", "Peer saved", "success")
|
||||
}else{
|
||||
this.dashboardConfigurationStore.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
this.$emit("refresh")
|
||||
})
|
||||
},
|
||||
resetPeerData(type){
|
||||
this.saving = true
|
||||
fetchPost(`/api/resetPeerData/${this.$route.params.id}`, {
|
||||
id: this.data.id,
|
||||
type: type
|
||||
}, (res) => {
|
||||
this.saving = false;
|
||||
if (res.status){
|
||||
this.dashboardConfigurationStore.newMessage("Server", "Peer data usage reset successfully", "success")
|
||||
}else{
|
||||
this.dashboardConfigurationStore.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
this.$emit("refresh")
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.reset();
|
||||
},
|
||||
mounted() {
|
||||
this.$el.querySelectorAll("input").forEach(x => {
|
||||
x.addEventListener("change", () => {
|
||||
this.dataChanged = true;
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal">
|
||||
<div class="card rounded-3 shadow flex-grow-1">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4 pb-2">
|
||||
<h4 class="mb-0">
|
||||
<LocaleText t="Peer Settings"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4" v-if="this.data">
|
||||
<div class="d-flex flex-column gap-2 mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Public Key"></LocaleText>
|
||||
</small>
|
||||
<small class="ms-auto"><samp>{{this.data.id}}</samp></small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="peer_name_textbox" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Name"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.name"
|
||||
id="peer_name_textbox" placeholder="">
|
||||
</div>
|
||||
<div>
|
||||
<label for="peer_notes_textbox" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Notes"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.notes"
|
||||
id="peer_notes_textbox" placeholder="">
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex position-relative">
|
||||
<label for="peer_private_key_textbox" class="form-label">
|
||||
<small class="text-muted"><LocaleText t="Private Key"></LocaleText>
|
||||
<code>
|
||||
<LocaleText t="(Required for QR Code and Download)"></LocaleText>
|
||||
</code></small>
|
||||
</label>
|
||||
<a role="button" class="ms-auto text-decoration-none toggleShowKey"
|
||||
@click="this.showKey = !this.showKey"
|
||||
>
|
||||
<i class="bi" :class="[this.showKey ? 'bi-eye-slash-fill':'bi-eye-fill']"></i>
|
||||
</a>
|
||||
</div>
|
||||
<input :type="[this.showKey ? 'text':'password']" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.private_key"
|
||||
id="peer_private_key_textbox"
|
||||
style="padding-right: 40px">
|
||||
</div>
|
||||
<div>
|
||||
<label for="peer_allowed_ip_textbox" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Allowed IPs"></LocaleText>
|
||||
<code>
|
||||
<LocaleText t="(Required)"></LocaleText>
|
||||
</code></small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.allowed_ip"
|
||||
id="peer_allowed_ip_textbox">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="peer_endpoint_allowed_ips" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Endpoint Allowed IPs"></LocaleText>
|
||||
<code>
|
||||
<LocaleText t="(Required)"></LocaleText>
|
||||
</code></small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.endpoint_allowed_ip"
|
||||
id="peer_endpoint_allowed_ips">
|
||||
</div>
|
||||
<div>
|
||||
<label for="peer_DNS_textbox" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="DNS"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.DNS"
|
||||
id="peer_DNS_textbox">
|
||||
</div>
|
||||
<div class="accordion my-3" id="peerSettingsAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button rounded-3 collapsed" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#peerSettingsAccordionOptional">
|
||||
<LocaleText t="Optional Settings"></LocaleText>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="peerSettingsAccordionOptional" class="accordion-collapse collapse"
|
||||
data-bs-parent="#peerSettingsAccordion">
|
||||
<div class="accordion-body d-flex flex-column gap-2 mb-2">
|
||||
<div>
|
||||
<label for="peer_preshared_key_textbox" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Pre-Shared Key"></LocaleText></small>
|
||||
</label>
|
||||
<input type="text" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.preshared_key"
|
||||
id="peer_preshared_key_textbox">
|
||||
</div>
|
||||
<div>
|
||||
<label for="peer_mtu" class="form-label"><small class="text-muted">
|
||||
<LocaleText t="MTU"></LocaleText>
|
||||
</small></label>
|
||||
<input type="number" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.mtu"
|
||||
id="peer_mtu">
|
||||
</div>
|
||||
<div>
|
||||
<label for="peer_keep_alive" class="form-label">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Persistent Keepalive"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control form-control-sm rounded-3"
|
||||
:disabled="this.saving"
|
||||
v-model="this.data.keepalive"
|
||||
id="peer_keep_alive">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button class="btn bg-secondary-subtle border-secondary-subtle text-secondary-emphasis rounded-3 shadow ms-auto px-3 py-2"
|
||||
@click="this.reset()"
|
||||
:disabled="!this.dataChanged || this.saving">
|
||||
<i class="bi bi-arrow-clockwise me-2"></i>
|
||||
<LocaleText t="Reset"></LocaleText>
|
||||
</button>
|
||||
|
||||
<button class="btn bg-primary-subtle border-primary-subtle text-primary-emphasis rounded-3 px-3 py-2 shadow"
|
||||
:disabled="!this.dataChanged || this.saving"
|
||||
@click="this.savePeer()"
|
||||
>
|
||||
<i class="bi bi-save-fill me-2"></i>
|
||||
<LocaleText t="Save"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<strong>
|
||||
<LocaleText t="Reset Data Usage"></LocaleText>
|
||||
</strong>
|
||||
<div class="d-flex gap-2 ms-auto">
|
||||
<button class="btn bg-primary-subtle text-primary-emphasis rounded-3 flex-grow-1 shadow-sm"
|
||||
@click="this.resetPeerData('total')"
|
||||
>
|
||||
<i class="bi bi-arrow-down-up me-2"></i>
|
||||
<LocaleText t="Total"></LocaleText>
|
||||
</button>
|
||||
<button class="btn bg-primary-subtle text-primary-emphasis rounded-3 flex-grow-1 shadow-sm"
|
||||
@click="this.resetPeerData('receive')"
|
||||
>
|
||||
<i class="bi bi-arrow-down me-2"></i>
|
||||
<LocaleText t="Received"></LocaleText>
|
||||
</button>
|
||||
<button class="btn bg-primary-subtle text-primary-emphasis rounded-3 flex-grow-1 shadow-sm"
|
||||
@click="this.resetPeerData('sent')"
|
||||
>
|
||||
<i class="bi bi-arrow-up me-2"></i>
|
||||
<LocaleText t="Sent"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toggleShowKey{
|
||||
position: absolute;
|
||||
top: 35px;
|
||||
right: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,257 @@
|
||||
<script>
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import PeerSettingsDropdownTool
|
||||
from "@/components/configurationComponents/peerSettingsDropdownComponents/peerSettingsDropdownTool.vue";
|
||||
import PeerTagSelectDropdown
|
||||
from "@/components/configurationComponents/peerSettingsDropdownComponents/peerTagSelectDropdown.vue";
|
||||
import {onMounted} from "vue";
|
||||
|
||||
export default {
|
||||
name: "peerSettingsDropdown",
|
||||
components: {PeerTagSelectDropdown, PeerSettingsDropdownTool, LocaleText},
|
||||
setup(){
|
||||
const dashboardStore = DashboardConfigurationStore()
|
||||
return {dashboardStore}
|
||||
},
|
||||
props: {
|
||||
Peer: Object, ConfigurationInfo: Object, dropup: Boolean
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
deleteBtnDisabled: false,
|
||||
restrictBtnDisabled: false,
|
||||
allowAccessBtnDisabled: false,
|
||||
confirmDelete: false,
|
||||
height: 0
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.height = document.querySelector("#peerDropdown").clientHeight
|
||||
},
|
||||
methods: {
|
||||
downloadPeer(){
|
||||
fetchGet("/api/downloadPeer/"+this.$route.params.id, {
|
||||
id: this.Peer.id
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
const blob = new Blob([res.data.file], { type: "text/conf" });
|
||||
const jsonObjectUrl = URL.createObjectURL(blob);
|
||||
const filename = `${res.data.fileName}.conf`;
|
||||
const anchorEl = document.createElement("a");
|
||||
anchorEl.href = jsonObjectUrl;
|
||||
anchorEl.download = filename;
|
||||
anchorEl.click();
|
||||
this.dashboardStore.newMessage("WGDashboard", "Peer download started", "success")
|
||||
}else{
|
||||
this.dashboardStore.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
})
|
||||
},
|
||||
downloadQRCode(emit){
|
||||
fetchGet("/api/downloadPeer/"+this.$route.params.id, {
|
||||
id: this.Peer.id
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.$emit(emit, res.data.file)
|
||||
}else{
|
||||
this.dashboardStore.newMessage("Server", res.message, "danger")
|
||||
}
|
||||
})
|
||||
},
|
||||
deletePeer(){
|
||||
this.deleteBtnDisabled = true
|
||||
fetchPost(`/api/deletePeers/${this.$route.params.id}`, {
|
||||
peers: [this.Peer.id]
|
||||
}, (res) => {
|
||||
this.dashboardStore.newMessage("Server", res.message, res.status ? "success":"danger")
|
||||
this.$emit("refresh")
|
||||
this.deleteBtnDisabled = false
|
||||
})
|
||||
},
|
||||
restrictPeer(){
|
||||
this.restrictBtnDisabled = true
|
||||
fetchPost(`/api/restrictPeers/${this.$route.params.id}`, {
|
||||
peers: [this.Peer.id]
|
||||
}, (res) => {
|
||||
this.dashboardStore.newMessage("Server", res.message, res.status ? "success":"danger")
|
||||
this.$emit("refresh")
|
||||
this.restrictBtnDisabled = false
|
||||
})
|
||||
},
|
||||
allowAccessPeer(){
|
||||
this.allowAccessBtnDisabled = true
|
||||
fetchPost(`/api/allowAccessPeers/${this.$route.params.id}`, {
|
||||
peers: [this.Peer.id]
|
||||
}, (res) => {
|
||||
this.dashboardStore.newMessage("Server", res.message, res.status ? "success":"danger")
|
||||
this.$emit("refresh")
|
||||
this.allowAccessBtnDisabled = false
|
||||
})
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
:class="{'dropup': dropup}"
|
||||
class="dropdown-menu mt-2 shadow-lg d-block rounded-3" id="peerDropdown" style="max-width: 200px">
|
||||
<template v-if="!this.Peer.restricted">
|
||||
<template v-if="!this.confirmDelete">
|
||||
<template v-if="this.Peer.status === 'running'">
|
||||
<li style="font-size: 0.8rem; padding-left: var(--bs-dropdown-item-padding-x); padding-right: var(--bs-dropdown-item-padding-x);">
|
||||
<span class="text-body d-flex">
|
||||
<i class="bi bi-box-arrow-in-right"></i>
|
||||
<span class="ms-auto">
|
||||
{{this.Peer.endpoint}}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
</template>
|
||||
<template v-if="!this.Peer.private_key">
|
||||
<li>
|
||||
<small class="w-100 dropdown-item text-muted"
|
||||
style="white-space: break-spaces; font-size: 0.7rem">
|
||||
<LocaleText t="Download & QR Code is not available due to no private key set for this peer"></LocaleText>
|
||||
</small>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else>
|
||||
<li>
|
||||
<div class="text-center text-muted">
|
||||
|
||||
</div>
|
||||
<div class="d-flex" style="padding-left: var(--bs-dropdown-item-padding-x); padding-right: var(--bs-dropdown-item-padding-x);">
|
||||
<PeerSettingsDropdownTool icon="bi-download"
|
||||
title="Download"
|
||||
@click="this.downloadPeer()"></PeerSettingsDropdownTool>
|
||||
<PeerSettingsDropdownTool icon="bi-qr-code"
|
||||
title="QR Code"
|
||||
@click="this.$emit('qrcode')"></PeerSettingsDropdownTool>
|
||||
<PeerSettingsDropdownTool icon="bi-body-text"
|
||||
title="Configuration File"
|
||||
@click="this.$emit('configurationFile')"></PeerSettingsDropdownTool>
|
||||
<PeerSettingsDropdownTool icon="bi-share"
|
||||
title="Share Peer"
|
||||
@click="this.$emit('share')"></PeerSettingsDropdownTool>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex" role="button"
|
||||
@click="this.$emit('setting')"
|
||||
>
|
||||
<i class="me-auto bi bi-pen"></i> <LocaleText t="Peer Settings"></LocaleText>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex" role="button"
|
||||
@click="this.$emit('jobs')"
|
||||
>
|
||||
<i class="me-auto bi bi-app-indicator"></i> <LocaleText t="Schedule Jobs"></LocaleText>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex" role="button"
|
||||
@click="this.$emit('assign')"
|
||||
>
|
||||
<i class="me-auto bi bi-diagram-2"></i> <LocaleText t="Assign Peer"></LocaleText>
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown dropstart">
|
||||
<a class="dropdown-item d-flex " role="button"
|
||||
data-bs-auto-close="outside"
|
||||
data-bs-toggle="dropdown"
|
||||
|
||||
>
|
||||
<i class="me-auto bi bi-tag"></i> <LocaleText t="Tag Peer"></LocaleText>
|
||||
</a>
|
||||
<PeerTagSelectDropdown
|
||||
@update="this.$emit('refresh')"
|
||||
:Peer="Peer" :ConfigurationInfo="ConfigurationInfo"></PeerTagSelectDropdown>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex text-warning"
|
||||
@click="this.restrictPeer()"
|
||||
:class="{disabled: this.restrictBtnDisabled}"
|
||||
role="button">
|
||||
<i class="me-auto bi bi-lock"></i>
|
||||
<LocaleText t="Restrict Access" v-if="!this.restrictBtnDisabled"></LocaleText>
|
||||
<LocaleText t="Restricting..." v-else></LocaleText>
|
||||
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex fw-bold text-danger"
|
||||
@click="this.confirmDelete = true"
|
||||
:class="{disabled: this.deleteBtnDisabled}"
|
||||
role="button">
|
||||
<i class="me-auto bi bi-trash"></i>
|
||||
<LocaleText t="Delete" v-if="!this.deleteBtnDisabled"></LocaleText>
|
||||
<LocaleText t="Deleting..." v-else></LocaleText>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else>
|
||||
<li class="confirmDelete">
|
||||
<p style="white-space: break-spaces" class="mb-2 d-block fw-bold">
|
||||
<LocaleText t="Are you sure to delete this peer?"></LocaleText>
|
||||
</p>
|
||||
<div class="d-flex w-100 gap-2">
|
||||
<button
|
||||
@click="this.deletePeer()"
|
||||
:disabled="this.deleteBtnDisabled"
|
||||
class="flex-grow-1 ms-auto btn btn-sm bg-danger">
|
||||
<LocaleText t="Yes"></LocaleText>
|
||||
</button>
|
||||
<button
|
||||
:disabled="this.deleteBtnDisabled"
|
||||
@click="this.confirmDelete = false"
|
||||
class="flex-grow-1 btn btn-sm bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle">
|
||||
<LocaleText t="No"></LocaleText>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<li>
|
||||
<a class="dropdown-item d-flex text-warning"
|
||||
|
||||
@click="this.allowAccessPeer()"
|
||||
:class="{disabled: this.allowAccessBtnDisabled}"
|
||||
role="button">
|
||||
<i class="me-auto bi bi-unlock"></i>
|
||||
<LocaleText t="Allow Access" v-if="!this.allowAccessBtnDisabled"></LocaleText>
|
||||
<LocaleText t="Allowing Access..." v-else></LocaleText>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dropdown-menu{
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.dropdown-menu.dropup{
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.dropdown-item.disabled, .dropdown-item:disabled{
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.confirmDelete{
|
||||
padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
const props = defineProps({
|
||||
icon: String,
|
||||
title: String
|
||||
})
|
||||
const emit = defineEmits(["click"])
|
||||
|
||||
const show = ref(false)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a class="dropdown-item text-center px-0 rounded-3 position-relative" role="button"
|
||||
@mouseenter="show = true"
|
||||
@mouseleave="show = false"
|
||||
@click="emit('click')">
|
||||
<i class="me-auto bi" :class="icon"></i>
|
||||
<Transition name="zoomReversed">
|
||||
<span
|
||||
v-if="show"
|
||||
class="position-absolute d-block p-1 px-2 bg-body text-body rounded-3 border shadow">
|
||||
<small>
|
||||
<LocaleText :t="title"></LocaleText>
|
||||
</small>
|
||||
</span>
|
||||
</Transition>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
span{
|
||||
top: -34px;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import PeerTagBadge from "@/components/configurationComponents/peerTagBadge.vue";
|
||||
import {reactive, watch} from "vue";
|
||||
|
||||
const props = defineProps(['Peer', 'ConfigurationInfo'])
|
||||
const groups = reactive({...props.ConfigurationInfo.Info.PeerGroups})
|
||||
import { fetchPost } from "@/utilities/fetch.js"
|
||||
const emits = defineEmits(['update'])
|
||||
watch(() => groups, (newVal) => {
|
||||
fetchPost("/api/updateWireguardConfigurationInfo", {
|
||||
Name: props.ConfigurationInfo.Name,
|
||||
Key: "PeerGroups",
|
||||
Value: newVal
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
emits('update', groups)
|
||||
}
|
||||
})
|
||||
}, {
|
||||
deep: true
|
||||
})
|
||||
|
||||
const togglePeer = (groupId, peerId) => {
|
||||
if (groups[groupId].Peers.includes(peerId)){
|
||||
groups[groupId].Peers = groups[groupId].Peers.filter(x => x !== peerId)
|
||||
}else{
|
||||
groups[groupId].Peers.push(peerId)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-for="(group, groupId) in groups" >
|
||||
<a role="button"
|
||||
@click="togglePeer(groupId, Peer.id)"
|
||||
class="dropdown-item d-flex align-items-center">
|
||||
<i class="bi bi-check-circle-fill" v-if="group.Peers.includes(Peer.id)"></i>
|
||||
<i class="bi bi-circle" v-else></i>
|
||||
<PeerTagBadge
|
||||
class="ms-auto"
|
||||
:BackgroundColor="group.BackgroundColor" :GroupName="group.GroupName" :Icon="'bi-' + group.Icon"></PeerTagBadge>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup async>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {reactive, ref, watch} from "vue";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import PeerShareWithEmailBodyPreview
|
||||
from "@/components/configurationComponents/peerShareLinkComponents/peerShareWithEmailBodyPreview.vue";
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
const props = defineProps(['dataCopy', 'selectedPeer'])
|
||||
const emailIsReady = ref(false)
|
||||
await fetchGet("/api/email/ready", {}, (res) => {
|
||||
emailIsReady.value = res.status
|
||||
})
|
||||
const store = DashboardConfigurationStore()
|
||||
const email = reactive({
|
||||
Receiver: "",
|
||||
Body: store.Configuration.Email.email_template,
|
||||
Subject: "",
|
||||
IncludeAttachment: false,
|
||||
ConfigurationName: props.selectedPeer.configuration.Name,
|
||||
Peer: props.selectedPeer.id
|
||||
})
|
||||
|
||||
const livePreview = ref(false)
|
||||
|
||||
const sending = ref(false)
|
||||
|
||||
const sendEmail = async () => {
|
||||
sending.value = true;
|
||||
await fetchPost("/api/email/send", email, (res) => {
|
||||
if (res.status){
|
||||
store.newMessage("Server", "Email sent successfully!", "success")
|
||||
}else{
|
||||
store.newMessage("Server", `Email sent failed! Reason: ${res.message}`, "danger")
|
||||
}
|
||||
sending.value = false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const emits = defineEmits(['fullscreen', 'hide'])
|
||||
watch(livePreview, () => {
|
||||
emits('fullscreen', livePreview.value)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="emailIsReady">
|
||||
<div class="d-flex mb-3 align-items-center">
|
||||
<a role="button" class="d-flex text-decoration-none text-body text-muted" @click="emits('hide')">
|
||||
<i class="bi bi-chevron-left me-2"></i> Back
|
||||
</a>
|
||||
<h6 class="mb-0 ms-auto">
|
||||
<LocaleText t="Share with Email"></LocaleText>
|
||||
</h6>
|
||||
</div>
|
||||
<form class="d-flex gap-3 flex-column"
|
||||
@submit="(e) => {e.preventDefault(); sendEmail()}"
|
||||
>
|
||||
<div>
|
||||
<div class="position-relative" >
|
||||
<i class="bi bi-person-circle" style="position: absolute; top: 0.4rem; left: 0.75rem;"></i>
|
||||
<input type="email" class="form-control rounded-top-3 rounded-bottom-0"
|
||||
style="padding-left: calc( 0.75rem + 24px )"
|
||||
v-model="email.Receiver"
|
||||
:disabled="sending"
|
||||
:placeholder="GetLocale('Who are you sending to?')"
|
||||
required
|
||||
id="email_receiver" aria-describedby="emailHelp">
|
||||
</div>
|
||||
<div class="position-relative">
|
||||
<i class="bi bi-hash" style="position: absolute; top: 0.4rem; left: 0.75rem;"></i>
|
||||
<input type="text" class="form-control rounded-0 border-top-0 border-bottom-0"
|
||||
style="padding-left: calc( 0.75rem + 24px )"
|
||||
:placeholder="GetLocale('What\'s the subject?')"
|
||||
:disabled="sending"
|
||||
v-model="email.Subject"
|
||||
id="email_subject" aria-describedby="emailHelp">
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<div :class="[livePreview ? 'col-6' : 'col-12']">
|
||||
<textarea class="form-control rounded-top-0 rounded-bottom-0 font-monospace border-bottom-0"
|
||||
v-model="email.Body"
|
||||
:disabled="sending"
|
||||
:placeholder="GetLocale('What\'s the body?')"
|
||||
style="height: 400px; max-height: 400px;"></textarea>
|
||||
</div>
|
||||
<div class="col-6" v-if="livePreview">
|
||||
<PeerShareWithEmailBodyPreview
|
||||
:email="email"
|
||||
:selectedPeer="selectedPeer"
|
||||
>
|
||||
</PeerShareWithEmailBodyPreview>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-top-0 rounded-top-0 rounded-bottom-3 bg-body-tertiary"
|
||||
style="border: var(--bs-border-width) solid var(--bs-border-color)">
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
v-model="livePreview"
|
||||
role="switch" id="livePreview">
|
||||
<label class="form-check-label" for="livePreview">
|
||||
<LocaleText t="Live Preview"></LocaleText>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
v-model="email.IncludeAttachment"
|
||||
role="switch" id="includeAttachment">
|
||||
<label class="form-check-label" for="includeAttachment">
|
||||
<LocaleText t="Include configuration file as an attachment"></LocaleText>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
:disabled="sending"
|
||||
class="btn bg-primary-subtle text-primary-emphasis border-primary-subtle rounded-3">
|
||||
<span v-if="!sending">
|
||||
<i class="bi bi-send me-2"></i><LocaleText t="Send"></LocaleText>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
<LocaleText t="Sending..."></LocaleText>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else>
|
||||
<small>
|
||||
<LocaleText t="SMTP is not configured, please navigate to "></LocaleText>
|
||||
<RouterLink to="/settings">
|
||||
<LocaleText t="Settings"></LocaleText>
|
||||
</RouterLink>
|
||||
<LocaleText t=" to finish setup"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
textarea:focus, input:focus{
|
||||
box-shadow: none;
|
||||
border-color: var(--bs-border-color) !important;
|
||||
}
|
||||
|
||||
textarea{
|
||||
padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup>
|
||||
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
import {ref, watch} from "vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
|
||||
const props = defineProps(['email', 'selectedPeer'])
|
||||
const preview = ref("")
|
||||
const error = ref(false)
|
||||
const errorMsg = ref("")
|
||||
|
||||
|
||||
const getPreview = async () => {
|
||||
if (props.email){
|
||||
error.value = false;
|
||||
|
||||
await fetchPost('/api/email/preview', {
|
||||
Subject: props.email.Subject,
|
||||
Body: props.email.Body,
|
||||
ConfigurationName: props.selectedPeer.configuration.Name,
|
||||
Peer: props.selectedPeer.id
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
preview.value = res.data;
|
||||
}else{
|
||||
preview.value = ("")
|
||||
errorMsg.value = res.message
|
||||
}
|
||||
error.value = !res.status
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await getPreview();
|
||||
let timeout = undefined
|
||||
watch(() => {
|
||||
return props.email
|
||||
}, async () => {
|
||||
if (timeout === undefined){
|
||||
timeout = setTimeout(async () => {
|
||||
await getPreview();
|
||||
}, 500)
|
||||
}else{
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(async () => {
|
||||
await getPreview();
|
||||
}, 500)
|
||||
}
|
||||
}, {
|
||||
deep: true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card rounded-0 border-start-0 border-bottom-0 bg-body-secondary" style="height: 400px; overflow: scroll">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-danger rounded-3" v-if="error && email.Body">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<span class="font-monospace">
|
||||
{{errorMsg}}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="preview">
|
||||
<strong><LocaleText t="Subject"></LocaleText>: </strong>{{ preview.Subject }}
|
||||
</div>
|
||||
<hr>
|
||||
<div :class="{'opacity-50': error}" v-bind:innerText="preview.Body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card{
|
||||
border-color: var(--bs-border-color) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<script>
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {fetchPost} from "@/utilities/fetch.js";
|
||||
import dayjs from "dayjs";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import { VueDatePicker } from '@vuepic/vue-datepicker';
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import PeerShareWithEmail from "@/components/configurationComponents/peerShareLinkComponents/peerShareWithEmail.vue";
|
||||
|
||||
|
||||
export default {
|
||||
name: "peerShareLinkModal",
|
||||
props: {
|
||||
selectedPeer: Object
|
||||
},
|
||||
components: {
|
||||
PeerShareWithEmail,
|
||||
LocaleText,
|
||||
VueDatePicker
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
dataCopy: undefined,
|
||||
loading: false,
|
||||
fullscreen: false,
|
||||
shareWithEmail: false
|
||||
}
|
||||
},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
return {store}
|
||||
},
|
||||
mounted() {
|
||||
this.dataCopy = JSON.parse(JSON.stringify(this.selectedPeer.ShareLink)).at(0);
|
||||
},
|
||||
watch: {
|
||||
'selectedPeer.ShareLink': {
|
||||
deep: true,
|
||||
handler(newVal, oldVal){
|
||||
if (oldVal.length !== newVal.length){
|
||||
this.dataCopy = JSON.parse(JSON.stringify(this.selectedPeer.ShareLink)).at(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
startSharing(){
|
||||
this.loading = true;
|
||||
fetchPost("/api/sharePeer/create", {
|
||||
Configuration: this.selectedPeer.configuration.Name,
|
||||
Peer: this.selectedPeer.id,
|
||||
ExpireDate: dayjs().add(7, 'd').format("YYYY-MM-DD HH:mm:ss")
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.selectedPeer.ShareLink = res.data;
|
||||
this.dataCopy = res.data.at(0);
|
||||
}else{
|
||||
this.store.newMessage("Server",
|
||||
"Share link failed to create. Reason: " + res.message, "danger")
|
||||
}
|
||||
this.loading = false;
|
||||
})
|
||||
},
|
||||
updateLinkExpireDate(){
|
||||
fetchPost("/api/sharePeer/update", this.dataCopy, (res) => {
|
||||
if (res.status){
|
||||
this.dataCopy = res.data.at(0)
|
||||
this.selectedPeer.ShareLink = res.data;
|
||||
this.store.newMessage("Server", "Link expire date updated", "success")
|
||||
}else{
|
||||
this.store.newMessage("Server",
|
||||
"Link expire date failed to update. Reason: " + res.message, "danger")
|
||||
}
|
||||
this.loading = false
|
||||
});
|
||||
},
|
||||
stopSharing(){
|
||||
this.loading = true;
|
||||
this.dataCopy.ExpireDate = dayjs().format("YYYY-MM-DD HH:mm:ss")
|
||||
this.updateLinkExpireDate()
|
||||
},
|
||||
parseTime(modelData){
|
||||
if(modelData){
|
||||
this.dataCopy.ExpireDate = dayjs(modelData).format("YYYY-MM-DD HH:mm:ss");
|
||||
}else{
|
||||
this.dataCopy.ExpireDate = undefined
|
||||
}
|
||||
this.updateLinkExpireDate()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getUrl(){
|
||||
const crossServer = this.store.getActiveCrossServer();
|
||||
if(crossServer){
|
||||
return `${crossServer.host}/${this.$router.resolve(
|
||||
{path: "/share", query: {"ShareID": this.dataCopy.ShareID}}).href}`
|
||||
}
|
||||
|
||||
return window.location.origin
|
||||
+ window.location.pathname
|
||||
+ this.$router.resolve(
|
||||
{path: "/share", query: {"ShareID": this.dataCopy.ShareID}}).href;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll">
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal" :style="[ this.fullscreen ? 'width: 100%' : 'width: 700px']">
|
||||
<div class="card rounded-3 shadow flex-grow-1">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 border-0 p-4">
|
||||
<h4 class="mb-0">
|
||||
<LocaleText t="Share Peer"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto" @click="this.$emit('close')"></button>
|
||||
</div>
|
||||
<div class="card-body px-4 pb-4" v-if="this.selectedPeer.ShareLink">
|
||||
<div v-if="!this.dataCopy">
|
||||
<h6 class="mb-3 text-muted">
|
||||
<LocaleText t="Currently the peer is not sharing"></LocaleText>
|
||||
</h6>
|
||||
<button
|
||||
@click="this.startSharing()"
|
||||
:disabled="this.loading"
|
||||
class="w-100 btn bg-success-subtle text-success-emphasis border-1 border-success-subtle rounded-3 shadow-sm">
|
||||
<span :class="{'animate__animated animate__flash animate__infinite animate__slower': this.loading}">
|
||||
<i class="bi bi-send-fill me-2" ></i>
|
||||
</span>
|
||||
<LocaleText t="Sharing..." v-if="this.loading"></LocaleText>
|
||||
<LocaleText t="Start Sharing" v-else></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="!shareWithEmail">
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
<a :href="this.getUrl"
|
||||
class="text-decoration-none" target="_blank">
|
||||
{{ getUrl }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-2 mb-3">
|
||||
<small>
|
||||
<i class="bi bi-calendar me-2"></i>
|
||||
<LocaleText t="Expire At"></LocaleText>
|
||||
</small>
|
||||
<VueDatePicker
|
||||
:is24="true"
|
||||
:min-date="new Date()"
|
||||
:model-value="this.dataCopy.ExpireDate"
|
||||
@update:model-value="this.parseTime" time-picker-inline
|
||||
format="yyyy-MM-dd HH:mm:ss"
|
||||
preview-format="yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
:dark="this.store.Configuration.Server.dashboard_theme === 'dark'"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-column flex-sm-row">
|
||||
<button
|
||||
style="flex: 1 1 0"
|
||||
@click="this.stopSharing()"
|
||||
:disabled="this.loading"
|
||||
class="w-100 btn bg-danger-subtle text-danger-emphasis border-1 border-danger-subtle rounded-3 shadow-sm">
|
||||
<span :class="{'animate__animated animate__flash animate__infinite animate__slower': this.loading}">
|
||||
<i class="bi bi-send-slash-fill me-2" ></i>
|
||||
</span>
|
||||
<LocaleText t="Stop Sharing..." v-if="this.loading"></LocaleText>
|
||||
<LocaleText t="Stop Sharing" v-else></LocaleText>
|
||||
</button>
|
||||
<button
|
||||
style="flex: 1 1 0"
|
||||
@click="shareWithEmail = true"
|
||||
class="btn bg-primary-subtle text-primary-emphasis border-primary-subtle rounded-3">
|
||||
<i class="bi bi-envelope me-2"></i><LocaleText t="Share with Email"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Suspense v-else>
|
||||
<PeerShareWithEmail
|
||||
@hide="shareWithEmail = false"
|
||||
@fullscreen="(f) => { this.fullscreen = f; }"
|
||||
:selectedPeer="selectedPeer" :dataCopy="dataCopy"></PeerShareWithEmail>
|
||||
<template #fallback>
|
||||
<h6 class="text-muted">
|
||||
<span class="spinner-border me-2 spinner-border-sm" role="status"></span>
|
||||
<LocaleText t="Checking SMTP Configuration..."></LocaleText>
|
||||
</h6>
|
||||
</template>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,241 @@
|
||||
<script setup lang="ts">
|
||||
import {reactive, ref, watch} from "vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import bootstrapIcons from "bootstrap-icons/font/bootstrap-icons.json"
|
||||
|
||||
const predefinedColors = {
|
||||
"blue-100": "#cfe2ff",
|
||||
"blue-200": "#9ec5fe",
|
||||
"blue-300": "#6ea8fe",
|
||||
"blue-400": "#3d8bfd",
|
||||
"blue-500": "#0d6efd",
|
||||
"blue-600": "#0a58ca",
|
||||
"blue-700": "#084298",
|
||||
"blue-800": "#052c65",
|
||||
"blue-900": "#031633",
|
||||
"indigo-100": "#e0cffc",
|
||||
"indigo-200": "#c29ffa",
|
||||
"indigo-300": "#a370f7",
|
||||
"indigo-400": "#8540f5",
|
||||
"indigo-500": "#6610f2",
|
||||
"indigo-600": "#520dc2",
|
||||
"indigo-700": "#3d0a91",
|
||||
"indigo-800": "#290661",
|
||||
"indigo-900": "#140330",
|
||||
"purple-100": "#e2d9f3",
|
||||
"purple-200": "#c5b3e6",
|
||||
"purple-300": "#a98eda",
|
||||
"purple-400": "#8c68cd",
|
||||
"purple-500": "#6f42c1",
|
||||
"purple-600": "#59359a",
|
||||
"purple-700": "#432874",
|
||||
"purple-800": "#2c1a4d",
|
||||
"purple-900": "#160d27",
|
||||
"pink-100": "#f7d6e6",
|
||||
"pink-200": "#efadce",
|
||||
"pink-300": "#e685b5",
|
||||
"pink-400": "#de5c9d",
|
||||
"pink-500": "#d63384",
|
||||
"pink-600": "#ab296a",
|
||||
"pink-700": "#801f4f",
|
||||
"pink-800": "#561435",
|
||||
"pink-900": "#2b0a1a",
|
||||
"red-100": "#f8d7da",
|
||||
"red-200": "#f1aeb5",
|
||||
"red-300": "#ea868f",
|
||||
"red-400": "#e35d6a",
|
||||
"red-500": "#dc3545",
|
||||
"red-600": "#b02a37",
|
||||
"red-700": "#842029",
|
||||
"red-800": "#58151c",
|
||||
"red-900": "#2c0b0e",
|
||||
"orange-100": "#ffe5d0",
|
||||
"orange-200": "#fecba1",
|
||||
"orange-300": "#feb272",
|
||||
"orange-400": "#fd9843",
|
||||
"orange-500": "#fd7e14",
|
||||
"orange-600": "#ca6510",
|
||||
"orange-700": "#984c0c",
|
||||
"orange-800": "#653208",
|
||||
"orange-900": "#331904",
|
||||
"yellow-100": "#fff3cd",
|
||||
"yellow-200": "#ffe69c",
|
||||
"yellow-300": "#ffda6a",
|
||||
"yellow-400": "#ffcd39",
|
||||
"yellow-500": "#ffc107",
|
||||
"yellow-600": "#cc9a06",
|
||||
"yellow-700": "#997404",
|
||||
"yellow-800": "#664d03",
|
||||
"yellow-900": "#332701",
|
||||
"green-100": "#d1e7dd",
|
||||
"green-200": "#a3cfbb",
|
||||
"green-300": "#75b798",
|
||||
"green-400": "#479f76",
|
||||
"green-500": "#198754",
|
||||
"green-600": "#146c43",
|
||||
"green-700": "#0f5132",
|
||||
"green-800": "#0a3622",
|
||||
"green-900": "#051b11",
|
||||
"teal-100": "#d2f4ea",
|
||||
"teal-200": "#a6e9d5",
|
||||
"teal-300": "#79dfc1",
|
||||
"teal-400": "#4dd4ac",
|
||||
"teal-500": "#20c997",
|
||||
"teal-600": "#1aa179",
|
||||
"teal-700": "#13795b",
|
||||
"teal-800": "#0d503c",
|
||||
"teal-900": "#06281e",
|
||||
"cyan-100": "#cff4fc",
|
||||
"cyan-200": "#9eeaf9",
|
||||
"cyan-300": "#6edff6",
|
||||
"cyan-400": "#3dd5f3",
|
||||
"cyan-500": "#0dcaf0",
|
||||
"cyan-600": "#0aa2c0",
|
||||
"cyan-700": "#087990",
|
||||
"cyan-800": "#055160",
|
||||
"cyan-900": "#032830",
|
||||
"gray-100": "#f8f9fa",
|
||||
"gray-200": "#e9ecef",
|
||||
"gray-300": "#dee2e6",
|
||||
"gray-400": "#ced4da",
|
||||
"gray-500": "#adb5bd",
|
||||
"gray-600": "#6c757d",
|
||||
"gray-700": "#495057",
|
||||
"gray-800": "#343a40",
|
||||
"gray-900": "#212529",
|
||||
"white": "#fff",
|
||||
"black": "#000",
|
||||
}
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"
|
||||
const store = WireguardConfigurationsStore()
|
||||
const props = defineProps(['configuration'])
|
||||
const groups = reactive({...props.configuration.Info.PeerGroups})
|
||||
import { v4 } from "uuid"
|
||||
import PeerTagSetting from "@/components/configurationComponents/peerTagComponents/peerTagSetting.vue";
|
||||
import PeerTagIconPicker from "@/components/configurationComponents/peerTagComponents/peerTagIconPicker.vue";
|
||||
import PeerTagColorPicker from "@/components/configurationComponents/peerTagComponents/peerTagColorPicker.vue";
|
||||
import { fetchPost } from "@/utilities/fetch.js"
|
||||
|
||||
const addGroup = () => {
|
||||
groups[v4().toString()] = {
|
||||
GroupName: "",
|
||||
Description: "",
|
||||
BackgroundColor: randomColor(),
|
||||
Icon: randomIcon(),
|
||||
Peers: []
|
||||
}
|
||||
}
|
||||
|
||||
const randomColor = () => {
|
||||
const keys = Object.keys(predefinedColors);
|
||||
const n = Math.floor(Math.random() * keys.length) + 1
|
||||
return predefinedColors[keys[n]]
|
||||
}
|
||||
|
||||
const randomIcon = () => {
|
||||
const keys = Object.keys(bootstrapIcons)
|
||||
const n = Math.floor(Math.random() * keys.length) + 1
|
||||
return keys[n]
|
||||
}
|
||||
|
||||
const iconPickerOpen = ref(false)
|
||||
const colorPickerOpen = ref(false)
|
||||
const selectedKey = ref("")
|
||||
const emits = defineEmits(['close', 'update'])
|
||||
watch(() => groups, (newVal) => {
|
||||
fetchPost("/api/updateWireguardConfigurationInfo", {
|
||||
Name: props.configuration.Name,
|
||||
Key: "PeerGroups",
|
||||
Value: newVal
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
emits('update', groups)
|
||||
}
|
||||
})
|
||||
}, {
|
||||
deep: true
|
||||
})
|
||||
|
||||
const edit = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card shadow rounded-3" id="peerTag">
|
||||
<div class="card-header">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="showAllPeers" v-model="store.Filter.ShowAllPeersWhenHiddenTags">
|
||||
<label class="form-check-label" for="showAllPeers">
|
||||
<small>
|
||||
<LocaleText t="Show All Peers"></LocaleText>
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-2" >
|
||||
<Transition name="zoom" mode="out-in">
|
||||
<div v-if="!iconPickerOpen && !colorPickerOpen">
|
||||
<div v-if="Object.keys(groups).length === 0" class="text-center text-muted">
|
||||
<small><LocaleText t="No tag"></LocaleText></small>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-2" v-else>
|
||||
<TransitionGroup name="slide-fade">
|
||||
<PeerTagSetting v-for="(group, key) in groups"
|
||||
:groupId="key"
|
||||
@delete="delete groups[key]; store.Filter.HiddenTags = store.Filter.HiddenTags.filter(x => x !== key)"
|
||||
@colorPickerOpen="colorPickerOpen = true; selectedKey = key"
|
||||
@iconPickerOpen="iconPickerOpen = true; selectedKey = key"
|
||||
:key="key"
|
||||
:edit="edit"
|
||||
:group="group"></PeerTagSetting>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
<PeerTagIconPicker
|
||||
v-else-if="iconPickerOpen"
|
||||
@close="iconPickerOpen = false"
|
||||
:group="groups[selectedKey]"></PeerTagIconPicker>
|
||||
<PeerTagColorPicker :colors="predefinedColors"
|
||||
@close="colorPickerOpen = false"
|
||||
:group="groups[selectedKey]"
|
||||
v-else-if="colorPickerOpen"></PeerTagColorPicker>
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="card-footer p-2 d-flex gap-2" >
|
||||
<template v-if="!edit">
|
||||
<button
|
||||
@click="emits('close')"
|
||||
class="btn btn-sm bg-secondary-subtle text-secondary-emphasis border-secondary-subtle rounded-3">
|
||||
<small><LocaleText t="Close"></LocaleText></small>
|
||||
</button>
|
||||
<button
|
||||
@click="edit = true"
|
||||
class="btn btn-sm bg-primary-subtle text-primary-emphasis border-primary-subtle rounded-3 ms-auto">
|
||||
<small><i class="bi bi-pen me-2"></i><LocaleText t="Edit"></LocaleText></small>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
<button
|
||||
@click="addGroup"
|
||||
class="btn btn-sm bg-primary-subtle text-primary-emphasis border-primary-subtle rounded-3 ">
|
||||
<small><i class="bi bi-plus-lg me-2"></i><LocaleText t="Tag"></LocaleText></small>
|
||||
</button>
|
||||
<button
|
||||
@click="edit = false"
|
||||
class="btn btn-sm bg-secondary-subtle text-secondary-emphasis border-secondary-subtle rounded-3 ms-auto">
|
||||
<small><LocaleText t="Done"></LocaleText></small>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#peerTag{
|
||||
width: 300px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"
|
||||
const props = defineProps(["BackgroundColor", "GroupName", "Icon"])
|
||||
const store = WireguardConfigurationsStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h6 class="mb-0">
|
||||
<span
|
||||
class="badge rounded-3 shadow"
|
||||
:style="{'background-color': BackgroundColor, 'color': store.colorText(BackgroundColor) }"
|
||||
>
|
||||
<i class="bi" :class="[Icon, GroupName ? 'me-2': '']" v-if="Icon"></i>{{ GroupName }}
|
||||
</span>
|
||||
</h6>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from "vue";
|
||||
const props = defineProps(['colors', 'group'])
|
||||
const emits = defineEmits(['close', 'select', ''])
|
||||
const searchString = ref("")
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
const store = WireguardConfigurationsStore();
|
||||
onMounted(() => {
|
||||
let ele = document.querySelector(".icon-grid div.active")
|
||||
if (ele){
|
||||
ele.parentElement.scrollTop =
|
||||
document.querySelector(".icon-grid div.active").offsetTop - 60
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-100 bg-body top-0 border rounded-2">
|
||||
<div class="p-2 d-grid icon-grid"
|
||||
style="grid-template-columns: repeat(auto-fit, minmax(30px, 30px)); gap: 3px; max-height: 300px; overflow-y: scroll">
|
||||
<div class="rounded-1 border icon d-flex"
|
||||
:class="{active: group.BackgroundColor === color}"
|
||||
style="cursor: pointer"
|
||||
:aria-label="name"
|
||||
:style="{'background-color': color}"
|
||||
:key="color"
|
||||
@click="group.BackgroundColor = color"
|
||||
v-for="(color, name) in colors">
|
||||
<i
|
||||
:style="{color: store.colorText(color)}"
|
||||
class="bi bi-check-circle m-auto" v-if="group.BackgroundColor === color"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 border-top d-flex gap-2">
|
||||
<button class="btn btn-sm btn-success rounded-2 ms-auto" @click="emits('close')">
|
||||
<LocaleText t="Done"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon{
|
||||
flex: 1;
|
||||
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import { GetLocale } from "@/utilities/locale.js"
|
||||
const props = defineProps(['group'])
|
||||
import bootstrapIcons from "bootstrap-icons/font/bootstrap-icons.json"
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
const emits = defineEmits(['close', 'select'])
|
||||
onMounted(() => {
|
||||
let ele = document.querySelector(".icon-grid div.active")
|
||||
if (ele){
|
||||
ele.parentElement.scrollTop =
|
||||
document.querySelector(".icon-grid div.active").offsetTop - 60
|
||||
}
|
||||
})
|
||||
const searchString = ref("")
|
||||
const searchIcon = computed(() => {
|
||||
if (searchString.value){
|
||||
|
||||
return [...Object.keys(bootstrapIcons).filter(
|
||||
x => x.includes(searchString.value.toLowerCase())
|
||||
)]
|
||||
}
|
||||
return Object.keys(bootstrapIcons)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-100 bg-body top-0 border rounded-2">
|
||||
<div class="p-2 d-flex align-items-center gap-2 border-bottom">
|
||||
<label>
|
||||
<i class="bi bi-search"></i>
|
||||
</label>
|
||||
<input v-model="searchString"
|
||||
:placeholder="GetLocale('Search Icon')"
|
||||
class="form-control form-control-sm rounded-2">
|
||||
</div>
|
||||
<div class="p-2 d-grid icon-grid"
|
||||
style="grid-template-columns: repeat(auto-fit, minmax(30px, 30px)); gap: 3px; max-height: 300px; overflow-y: scroll">
|
||||
<div class="rounded-1 border icon d-flex"
|
||||
:class="{'text-bg-success active' : group.Icon === iconName}"
|
||||
style="cursor: pointer"
|
||||
:key="iconName"
|
||||
@click="group.Icon = iconName"
|
||||
v-for="iconName in searchIcon">
|
||||
<i class="bi m-auto" :class="'bi-' + iconName"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 border-top d-flex gap-2">
|
||||
<button
|
||||
@click="group.Icon = ''"
|
||||
class="btn btn-sm btn-secondary rounded-2 ms-auto">
|
||||
<LocaleText t="Remove Icon"></LocaleText>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-success rounded-2" @click="emits('close')">
|
||||
<LocaleText t="Done"></LocaleText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon{
|
||||
flex: 1;
|
||||
min-width: 30px;
|
||||
max-width: 30px;
|
||||
width: 30px;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import { GetLocale } from "@/utilities/locale.js"
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js"
|
||||
import {ref} from "vue";
|
||||
const store = WireguardConfigurationsStore();
|
||||
const props = defineProps(['group', 'edit', 'groupId'])
|
||||
const emits = defineEmits(['delete', 'iconPickerOpen', 'colorPickerOpen', 'toggle'])
|
||||
const groupName = ref(props.group.GroupName)
|
||||
const toggleTag = () => {
|
||||
if (store.Filter.HiddenTags.includes(props.groupId)){
|
||||
store.Filter.HiddenTags = store.Filter.HiddenTags.filter(x => x !== props.groupId)
|
||||
}else{
|
||||
store.Filter.HiddenTags.push(props.groupId)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border rounded-3 p-2">
|
||||
<div
|
||||
class=" align-items-center overflow-scroll d-flex gap-2 position-relative">
|
||||
<button
|
||||
@click="emits('iconPickerOpen')"
|
||||
aria-label="Pick icon button"
|
||||
:class="{disabled: !edit}"
|
||||
class="d-flex align-items-center p-2 btn btn-sm border rounded-2">
|
||||
<i class="bi" :class="'bi-' + group.Icon" :aria-label="group.Icon" v-if="group.Icon"></i>
|
||||
<span style="white-space: nowrap" v-else>
|
||||
<LocaleText t="No Icon"></LocaleText>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
:class="{disabled: !edit}"
|
||||
aria-label="Pick color button"
|
||||
@click="emits('colorPickerOpen')"
|
||||
:style="{'background-color': group.BackgroundColor, 'color': store.colorText(group.BackgroundColor)}"
|
||||
class="d-flex align-items-center p-2 btn btn-sm border rounded-2">
|
||||
<i class="bi bi-eyedropper" ></i>
|
||||
</button>
|
||||
<input
|
||||
:disabled="!edit"
|
||||
v-model="groupName"
|
||||
@change="group.GroupName = groupName"
|
||||
:placeholder="GetLocale('Tag Name')"
|
||||
class="form-control form-control-sm p-2 rounded-2 w-100">
|
||||
<button
|
||||
v-if="edit"
|
||||
aria-label="Delete Tag Button" @click="emits('delete')"
|
||||
class="rounded-2 border p-2 btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
aria-label="Show / Hide Button"
|
||||
style="white-space: nowrap"
|
||||
:class="{active: !store.Filter.HiddenTags.includes(groupId)}"
|
||||
@click="toggleTag()"
|
||||
class="rounded-2 p-2 btn btn-sm btn-outline-primary">
|
||||
<i class="bi"
|
||||
:class="[!store.Filter.HiddenTags.includes(groupId) ? 'bi-eye-fill':'bi-eye-slash-fill']"
|
||||
></i>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script>
|
||||
export default {
|
||||
name: "restrictedPeers"
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="mb-4">
|
||||
<RouterLink to="peers" is="div" class="d-flex align-items-center gap-4 text-decoration-none">
|
||||
<h3 class="mb-0 text-body">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</h3>
|
||||
<h3 class="text-body mb-0">Restricted Peers</h3>
|
||||
</RouterLink>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,260 @@
|
||||
<script setup>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {computed, reactive, ref, useTemplateRef, watch} from "vue";
|
||||
import {fetchGet, fetchPost} from "@/utilities/fetch.js";
|
||||
import {useRoute} from "vue-router";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
|
||||
const props = defineProps({
|
||||
configurationPeers: Array
|
||||
})
|
||||
const deleteConfirmation = ref(false)
|
||||
const downloadConfirmation = ref(false)
|
||||
const selectedPeers = ref([])
|
||||
const selectPeersSearchInput = ref("")
|
||||
|
||||
const togglePeers = (id) => {
|
||||
if (selectedPeers.value.find(x => x === id)){
|
||||
selectedPeers.value = selectedPeers.value.filter(x => x !== id)
|
||||
}else{
|
||||
selectedPeers.value.push(id)
|
||||
}
|
||||
}
|
||||
|
||||
const searchPeers = computed(() => {
|
||||
if (deleteConfirmation.value || downloadConfirmation.value){
|
||||
return props.configurationPeers.filter(x =>
|
||||
selectedPeers.value.find(y => y === x.id)
|
||||
)
|
||||
}
|
||||
if (selectPeersSearchInput.value.length > 0){
|
||||
return props.configurationPeers.filter(x => {
|
||||
return x.id.includes(selectPeersSearchInput.value) || x.name.includes(selectPeersSearchInput.value)
|
||||
})
|
||||
}
|
||||
return props.configurationPeers
|
||||
})
|
||||
|
||||
watch(selectedPeers, () => {
|
||||
if (selectedPeers.value.length === 0){
|
||||
deleteConfirmation.value = false;
|
||||
downloadConfirmation.value = false;
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const dashboardStore = DashboardConfigurationStore()
|
||||
const emit = defineEmits(["refresh", "close"])
|
||||
const submitting = ref(false)
|
||||
const submitDelete = () => {
|
||||
submitting.value = true;
|
||||
fetchPost(`/api/deletePeers/${route.params.id}`, {
|
||||
peers: selectedPeers.value
|
||||
}, (res) => {
|
||||
dashboardStore.newMessage("Server", res.message, res.status ? "success":"danger")
|
||||
if (res.status){
|
||||
selectedPeers.value = []
|
||||
deleteConfirmation.value = false
|
||||
}
|
||||
emit("refresh")
|
||||
submitting.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const downloaded = reactive({
|
||||
success: [],
|
||||
failed: []
|
||||
})
|
||||
const cardBody = useTemplateRef('card-body');
|
||||
const el = useTemplateRef("sp")
|
||||
const submitDownload = async () => {
|
||||
downloadConfirmation.value = true
|
||||
for (const x of selectedPeers.value) {
|
||||
cardBody.value.scrollTo({
|
||||
top: el.value.find(y => y.dataset.id === x).offsetTop - 20,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
await fetchGet("/api/downloadPeer/"+route.params.id, {
|
||||
id: x
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
const blob = new Blob([res.data.file], { type: "text/plain" });
|
||||
const jsonObjectUrl = URL.createObjectURL(blob);
|
||||
const filename = `${res.data.fileName}.conf`;
|
||||
const anchorEl = document.createElement("a");
|
||||
anchorEl.href = jsonObjectUrl;
|
||||
anchorEl.download = filename;
|
||||
anchorEl.click();
|
||||
downloaded.success.push(x)
|
||||
}else{
|
||||
downloaded.failed.push(x)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const clearDownload = () => {
|
||||
downloaded.success = []
|
||||
downloaded.failed = []
|
||||
downloadConfirmation.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="peerSettingContainer w-100 h-100 position-absolute top-0 start-0 overflow-y-scroll" ref="selectPeersContainer">
|
||||
<div class="container d-flex h-100 w-100">
|
||||
<div class="m-auto modal-dialog-centered dashboardModal" style="width: 700px">
|
||||
<div class="card rounded-3 shadow flex-grow-1">
|
||||
<div class="card-header bg-transparent d-flex align-items-center gap-2 p-4 flex-column pb-3">
|
||||
<div class="mb-2 w-100 d-flex">
|
||||
<h4 class="mb-0">
|
||||
<LocaleText t="Select Peers"></LocaleText>
|
||||
</h4>
|
||||
<button type="button" class="btn-close ms-auto"
|
||||
@click="emit('close')"></button>
|
||||
</div>
|
||||
<div class="d-flex w-100 align-items-center gap-2">
|
||||
<div class="d-flex gap-3">
|
||||
<a role="button"
|
||||
v-if="!downloadConfirmation && selectedPeers.length !== configurationPeers.map(x => x.id).length"
|
||||
@click="selectedPeers = configurationPeers.map(x => x.id)"
|
||||
class="text-decoration-none text-body">
|
||||
<small>
|
||||
<i class="bi bi-check-all me-2"></i>
|
||||
<LocaleText t="Select All"></LocaleText>
|
||||
</small>
|
||||
</a>
|
||||
<a role="button" class="text-decoration-none text-body"
|
||||
@click="selectedPeers = []"
|
||||
v-if="selectedPeers.length > 0 && !downloadConfirmation">
|
||||
<small>
|
||||
<i class="bi bi-x-circle-fill me-2"></i>
|
||||
<LocaleText t="Clear Selection"></LocaleText>
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<label class="ms-auto" for="selectPeersSearchInput">
|
||||
<i class="bi bi-search"></i>
|
||||
</label>
|
||||
<input class="form-control form-control-sm rounded-3"
|
||||
v-model="selectPeersSearchInput"
|
||||
id="selectPeersSearchInput"
|
||||
style="width: 200px !important;" type="text">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body px-4 flex-grow-1 d-flex gap-2 flex-column position-relative"
|
||||
ref="card-body"
|
||||
style="overflow-y: scroll">
|
||||
<button type="button" class="btn w-100 peerBtn text-start rounded-3 d-flex align-items-center gap-3"
|
||||
@click="togglePeers(p.id)"
|
||||
:class="{active: selectedPeers.find(x => x === p.id)}"
|
||||
:key="p.id"
|
||||
:disabled="deleteConfirmation || downloadConfirmation"
|
||||
ref="sp"
|
||||
:data-id="p.id"
|
||||
v-for="p in searchPeers">
|
||||
<span v-if="!downloadConfirmation">
|
||||
<i class="bi"
|
||||
:class="[ selectedPeers.find(x => x === p.id) ? 'bi-check-circle-fill':'bi-circle']"
|
||||
></i>
|
||||
</span>
|
||||
<span class="d-flex flex-column">
|
||||
<small class="fw-bold">
|
||||
{{p.name ? p.name : "Untitled Peer"}}
|
||||
</small>
|
||||
<small class="text-muted">
|
||||
<samp>{{p.id}}</samp>
|
||||
</small>
|
||||
</span>
|
||||
<span v-if="downloadConfirmation" class="ms-auto">
|
||||
<span class="spinner-border spinner-border-sm" role="status"
|
||||
v-if="!downloaded.success.find(x => x === p.id) && !downloaded.failed.find(x => x === p.id)">
|
||||
</span>
|
||||
<i class="bi"
|
||||
v-else
|
||||
:class="[downloaded.failed.find(x => x === p.id) ? 'bi-x-circle-fill':'bi-check-circle-fill']"
|
||||
></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-footer px-4 py-3 gap-2 d-flex align-items-center">
|
||||
<template v-if="!deleteConfirmation && !downloadConfirmation">
|
||||
<button class="btn bg-primary-subtle text-primary-emphasis border-primary-subtle rounded-3"
|
||||
:disabled="selectedPeers.length === 0 || submitting"
|
||||
@click="submitDownload()"
|
||||
>
|
||||
<i class="bi bi-download"></i>
|
||||
</button>
|
||||
<span v-if="selectedPeers.length > 0" class="flex-grow-1 text-center">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
<LocaleText :t="selectedPeers.length + ' Peer' + (selectedPeers.length > 1 ? 's':'')"></LocaleText>
|
||||
</span>
|
||||
<button class="btn bg-danger-subtle text-danger-emphasis border-danger-subtle ms-auto rounded-3"
|
||||
@click="deleteConfirmation = true"
|
||||
:disabled="selectedPeers.length === 0 || submitting"
|
||||
>
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="downloadConfirmation">
|
||||
<strong v-if="downloaded.failed.length + downloaded.success.length < selectedPeers.length" class="flex-grow-1 text-center">
|
||||
<LocaleText t="Downloading" /> <LocaleText :t="selectedPeers.length + ' Peer' + (selectedPeers.length > 1 ? 's':'')"></LocaleText>...
|
||||
</strong>
|
||||
<template v-else>
|
||||
<strong>
|
||||
<LocaleText t="Download Finished"></LocaleText>
|
||||
</strong>
|
||||
<button
|
||||
@click="clearDownload()"
|
||||
class="btn bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle rounded-3 ms-auto">
|
||||
<LocaleText t="Done"></LocaleText>
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="deleteConfirmation">
|
||||
<button class="btn btn-danger rounded-3"
|
||||
:disabled="selectedPeers.length === 0 || submitting"
|
||||
@click="submitDelete()"
|
||||
>
|
||||
<LocaleText t="Yes"></LocaleText>
|
||||
</button>
|
||||
<strong v-if="selectedPeers.length > 0" class="flex-grow-1 text-center">
|
||||
<LocaleText t="Are you sure to delete"></LocaleText> <LocaleText :t="selectedPeers.length + ' Peer' + (selectedPeers.length > 1 ? 's':'')"></LocaleText>?
|
||||
</strong>
|
||||
<button class="btn bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle ms-auto rounded-3"
|
||||
:disabled="selectedPeers.length === 0 || submitting"
|
||||
@click="deleteConfirmation = false"
|
||||
>
|
||||
<LocaleText t="No"></LocaleText>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dashboardModal{
|
||||
height: calc(100% - 1rem) !important;
|
||||
}
|
||||
|
||||
@media screen and (min-height: 700px) {
|
||||
.card{
|
||||
height: 700px;
|
||||
}
|
||||
}
|
||||
|
||||
.peerBtn{
|
||||
border: var(--bs-border-width) solid var(--bs-border-color);
|
||||
}
|
||||
.peerBtn.active{
|
||||
border: var(--bs-border-width) solid var(--bs-body-color);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,152 @@
|
||||
<script>
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import ConfigurationCard from "@/components/configurationListComponents/configurationCard.vue";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import SystemStatus from "@/components/systemStatusComponents/systemStatusWidget.vue";
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
|
||||
export default {
|
||||
name: "configurationList",
|
||||
components: {SystemStatus, LocaleText, ConfigurationCard},
|
||||
async setup(){
|
||||
const wireguardConfigurationsStore = WireguardConfigurationsStore();
|
||||
return {wireguardConfigurationsStore}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
configurationLoaded: false,
|
||||
sort: {
|
||||
Name: GetLocale("Name"),
|
||||
Status: GetLocale("Status"),
|
||||
'DataUsage.Total': GetLocale("Total Usage")
|
||||
},
|
||||
currentSort: {
|
||||
key: "Name",
|
||||
order: "asc"
|
||||
},
|
||||
currentDisplay: "List",
|
||||
searchKey: ""
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
configurations(){
|
||||
return this.wireguardConfigurationsStore.sortConfigurations
|
||||
.filter(x => x.Name.toLowerCase().includes(this.searchKey) || x.PublicKey.includes(this.searchKey) || !this.searchKey)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
dotNotation(object, dotNotation){
|
||||
let result = dotNotation.split('.').reduce((o, key) => o && o[key], object)
|
||||
if (typeof result === "string"){
|
||||
return result.toLowerCase()
|
||||
}
|
||||
return result
|
||||
},
|
||||
updateSort(key){
|
||||
if (this.wireguardConfigurationsStore.CurrentSort.key === key){
|
||||
this.wireguardConfigurationsStore.CurrentSort.order =
|
||||
this.wireguardConfigurationsStore.CurrentSort.order === 'asc' ? 'desc' : 'asc'
|
||||
}else{
|
||||
this.wireguardConfigurationsStore.CurrentSort.key = key
|
||||
}
|
||||
},
|
||||
updateDisplay(key){
|
||||
if (this.wireguardConfigurationsStore.CurrentDisplay !== key){
|
||||
this.wireguardConfigurationsStore.CurrentDisplay = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-md-5 mt-3">
|
||||
<div class="container-fluid">
|
||||
<SystemStatus></SystemStatus>
|
||||
<div class="d-flex mb-4 configurationListTitle align-items-md-center gap-2 flex-column flex-md-row">
|
||||
<h2 class="text-body d-flex mb-0">
|
||||
<LocaleText t="WireGuard Configurations"></LocaleText>
|
||||
</h2>
|
||||
<RouterLink to="/new_configuration"
|
||||
class="ms-md-auto py-2 text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle">
|
||||
<i class="bi bi-plus-circle me-2"></i><LocaleText t="Configuration"></LocaleText>
|
||||
</RouterLink>
|
||||
<RouterLink to="/restore_configuration"
|
||||
class="py-2 text-decoration-none btn text-primary-emphasis bg-primary-subtle rounded-3 border-1 border-primary-subtle">
|
||||
<i class="bi bi-clock-history me-2"></i><LocaleText t="Restore"></LocaleText>
|
||||
</RouterLink>
|
||||
|
||||
</div>
|
||||
<Transition name="fade">
|
||||
<div class="text-body filter mb-3 d-flex gap-2 flex-column flex-md-row" v-if="this.wireguardConfigurationsStore.ConfigurationLoaded">
|
||||
<div class="d-flex align-items-center gap-3 align-items-center mb-3 mb-md-0">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Sort By"></LocaleText>
|
||||
</small>
|
||||
<div class="d-flex ms-auto ms-lg-0">
|
||||
<a role="button"
|
||||
@click="updateSort(sv)"
|
||||
:class="{'bg-primary-subtle text-primary-emphasis': this.wireguardConfigurationsStore.CurrentSort.key === sv}"
|
||||
class="px-2 py-1 rounded-3" v-for="(s, sv) in this.wireguardConfigurationsStore.SortOptions">
|
||||
<small>
|
||||
<i class="bi me-2"
|
||||
:class="[this.wireguardConfigurationsStore.CurrentSort.order === 'asc' ? 'bi-sort-up' : 'bi-sort-down']"
|
||||
v-if="this.wireguardConfigurationsStore.CurrentSort.key === sv"></i><LocaleText :t="s"></LocaleText>
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-items-center gap-3 align-items-center mb-3 mb-md-0 d-none d-lg-flex ">
|
||||
<small class="text-muted">
|
||||
<LocaleText t="Display as"></LocaleText>
|
||||
</small>
|
||||
<div class="d-flex ms-auto ms-lg-0">
|
||||
<a role="button"
|
||||
@click="updateDisplay(x.name)"
|
||||
v-for="x in [{name: 'List', key: 'list'}, {name: 'Grid', key: 'grid'}]"
|
||||
:class="{'bg-primary-subtle text-primary-emphasis': this.wireguardConfigurationsStore.CurrentDisplay === x.name}"
|
||||
class="px-2 py-1 rounded-3">
|
||||
<small>
|
||||
<i class="bi me-2" :class="'bi-' + x.key"></i> <LocaleText :t="x.name"></LocaleText>
|
||||
</small>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center ms-md-auto">
|
||||
<label for="configurationSearch" class="text-muted">
|
||||
<i class="bi bi-search me-2"></i>
|
||||
</label>
|
||||
<input class="form-control form-control-sm rounded-3"
|
||||
v-model="this.searchKey"
|
||||
id="configurationSearch">
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div class="row g-3 mb-2">
|
||||
<TransitionGroup name="fade">
|
||||
<p class="text-muted col-12"
|
||||
key="noConfiguration"
|
||||
v-if="this.wireguardConfigurationsStore.ConfigurationLoaded && this.wireguardConfigurationsStore.Configurations.length === 0">
|
||||
<LocaleText t="You don't have any WireGuard configurations yet. Please check the configuration folder or change it in Settings. By default the folder is /etc/wireguard."></LocaleText>
|
||||
</p>
|
||||
<ConfigurationCard
|
||||
:display="this.wireguardConfigurationsStore.CurrentDisplay"
|
||||
v-for="(c, index) in configurations"
|
||||
:delay="index*0.03 + 's'"
|
||||
v-else-if="this.wireguardConfigurationsStore.ConfigurationLoaded"
|
||||
:key="c.Name" :c="c"></ConfigurationCard>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter a{
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,128 @@
|
||||
<script>
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import ProtocolBadge from "@/components/protocolBadge.vue";
|
||||
|
||||
export default {
|
||||
name: "configurationCard",
|
||||
components: {ProtocolBadge, LocaleText},
|
||||
props: {
|
||||
c: {
|
||||
Name: String,
|
||||
Status: Boolean,
|
||||
PublicKey: String,
|
||||
PrivateKey: String
|
||||
},
|
||||
delay: String,
|
||||
display: String
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
configurationToggling: false
|
||||
}
|
||||
},
|
||||
setup(){
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore();
|
||||
return {dashboardConfigurationStore}
|
||||
},
|
||||
methods: {
|
||||
toggle(){
|
||||
this.configurationToggling = true;
|
||||
fetchGet("/api/toggleWireguardConfiguration", {
|
||||
configurationName: this.c.Name
|
||||
}, (res) => {
|
||||
if (res.status){
|
||||
this.dashboardConfigurationStore.newMessage("Server",
|
||||
`${this.c.Name} ${res.data ? 'is on':'is off'}`)
|
||||
}else{
|
||||
this.dashboardConfigurationStore.newMessage("Server",
|
||||
res.message, 'danger')
|
||||
}
|
||||
this.c.Status = res.data
|
||||
this.configurationToggling = false;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="col-12" :class="{'col-lg-6 col-xl-4': this.display === 'Grid'}">
|
||||
<div class="card conf_card rounded-3 shadow text-decoration-none">
|
||||
<RouterLink :to="'/configuration/' + c.Name + '/peers'" class="card-body d-flex align-items-center gap-3 flex-wrap text-decoration-none">
|
||||
<h6 class="mb-0"><span class="dot" :class="{active: c.Status}"></span></h6>
|
||||
<h6 class="card-title mb-0 d-flex align-items-center gap-2">
|
||||
<samp>{{c.Name}}</samp>
|
||||
<small>
|
||||
<ProtocolBadge :protocol="c.Protocol" :mini="true"></ProtocolBadge>
|
||||
</small>
|
||||
<small v-if="c.Info.Description">
|
||||
<span class="badge text-bg-info rounded-3 shadow">
|
||||
<i class="bi bi-pencil-fill me-2"></i>
|
||||
{{ c.Info.Description }}
|
||||
</span>
|
||||
</small>
|
||||
</h6>
|
||||
<h6 class="mb-0 ms-auto">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</h6>
|
||||
</RouterLink>
|
||||
<div class="card-footer d-flex gap-2 flex-column">
|
||||
<div class="row">
|
||||
<small class="col-6" :class="{'col-md-3': this.display === 'List'}">
|
||||
<i class="bi bi-arrow-down-up me-2"></i>{{c.DataUsage.Total > 0 ? c.DataUsage.Total.toFixed(4) : 0}} GB
|
||||
</small>
|
||||
<small class="text-primary-emphasis col-6" :class="{'col-md-3': this.display === 'List'}">
|
||||
<i class="bi bi-arrow-down me-2"></i>{{c.DataUsage.Receive > 0 ? c.DataUsage.Receive.toFixed(4) : 0}} GB
|
||||
</small>
|
||||
<small class="text-success-emphasis col-6" :class="{'col-md-3': this.display === 'List'}">
|
||||
<i class="bi bi-arrow-up me-2"></i>{{c.DataUsage.Sent > 0 ? c.DataUsage.Sent.toFixed(4) : 0}} GB
|
||||
</small>
|
||||
<small class="col-6" :class="{'col-md-3 text-md-end ': this.display === 'List'}">
|
||||
<span class="dot me-2" :class="{active: c.ConnectedPeers > 0}"></span>
|
||||
{{c.ConnectedPeers}} / {{c.TotalPeers}}
|
||||
<LocaleText t="Peers"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2 " :class="[this.display === 'Grid' ? 'flex-column': 'gap-lg-3 flex-column flex-lg-row']">
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<small class="text-muted">
|
||||
<strong>
|
||||
<LocaleText t="Public Key"></LocaleText>
|
||||
</strong>
|
||||
</small>
|
||||
<small class="mb-0 d-block d-lg-inline-block ">
|
||||
<samp style="line-break: anywhere">{{c.PublicKey}}</samp>
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-check form-switch ms-auto">
|
||||
<label class="form-check-label" style="cursor: pointer" :for="'switch' + c.PrivateKey">
|
||||
<LocaleText t="Turning Off..." v-if="!c.Status && this.configurationToggling"></LocaleText>
|
||||
<LocaleText t="Turning On..." v-else-if="c.Status && this.configurationToggling"></LocaleText>
|
||||
<LocaleText t="On" v-else-if="c.Status && !this.configurationToggling"></LocaleText>
|
||||
<LocaleText t="Off" v-else-if="!c.Status && !this.configurationToggling"></LocaleText>
|
||||
|
||||
|
||||
<span v-if="this.configurationToggling"
|
||||
class="spinner-border spinner-border-sm ms-2" aria-hidden="true"></span>
|
||||
</label>
|
||||
<input class="form-check-input"
|
||||
style="cursor: pointer"
|
||||
:disabled="this.configurationToggling"
|
||||
type="checkbox" role="switch" :id="'switch' + c.PrivateKey"
|
||||
@change="this.toggle()"
|
||||
v-model="c.Status"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active{
|
||||
transition-delay: v-bind(delay) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,130 @@
|
||||
<script>
|
||||
import "ol/ol.css"
|
||||
import Map from 'ol/Map.js';
|
||||
import OSM from 'ol/source/OSM.js';
|
||||
import TileLayer from 'ol/layer/Tile.js';
|
||||
import View from 'ol/View.js';
|
||||
import {Feature} from "ol";
|
||||
import {fromLonLat} from "ol/proj"
|
||||
import {LineString, Point} from "ol/geom"
|
||||
import {Circle, Fill, Stroke, Style, Text} from "ol/style.js";
|
||||
import {Vector} from "ol/layer"
|
||||
import {Vector as SourceVector} from "ol/source"
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
|
||||
export default {
|
||||
name: "osmap",
|
||||
props: {
|
||||
type: "",
|
||||
d: Object || Array
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
osmAvailable: true
|
||||
}
|
||||
},
|
||||
setup(){
|
||||
const store = DashboardConfigurationStore();
|
||||
return {
|
||||
store
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getLastLonLat(){
|
||||
if (this.type === 'traceroute'){
|
||||
const k = this.d.findLast(data => data.geo && data.geo.lat && data.geo.lon)
|
||||
if (k){
|
||||
return [k.geo.lon, k.geo.lat]
|
||||
}
|
||||
return [0,0]
|
||||
}
|
||||
return [this.d.geo.lon, this.d.geo.lat]
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
const osm = await fetch("https://tile.openstreetmap.org/",
|
||||
{ signal: AbortSignal.timeout(1500) })
|
||||
.then(res => {
|
||||
const map = new Map({
|
||||
target: 'map',
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new OSM(),
|
||||
}),
|
||||
],
|
||||
view: new View({
|
||||
center: fromLonLat(this.getLastLonLat()),
|
||||
zoom: this.type === 'traceroute' ? 3:10,
|
||||
}),
|
||||
});
|
||||
const coordinates = [];
|
||||
const vectorSource = new SourceVector();
|
||||
if (this.type === 'traceroute'){
|
||||
this.d.forEach(data => {
|
||||
if (data.geo && data.geo.lat && data.geo.lon) {
|
||||
const coordinate = fromLonLat([data.geo.lon, data.geo.lat]);
|
||||
coordinates.push(coordinate);
|
||||
const l = this.getLastLonLat();
|
||||
const marker = new Feature({
|
||||
geometry: new Point(coordinate),
|
||||
last: data.geo.lon === l[0] && data.geo.lat === l[1]
|
||||
});
|
||||
vectorSource.addFeature(marker);
|
||||
}
|
||||
})
|
||||
}
|
||||
else{
|
||||
const coordinate = fromLonLat([this.d.geo.lon, this.d.geo.lat])
|
||||
coordinates.push(coordinate);
|
||||
const marker = new Feature({
|
||||
geometry: new Point(coordinate)
|
||||
});
|
||||
vectorSource.addFeature(marker);
|
||||
}
|
||||
const lineString = new LineString(coordinates);
|
||||
const lineFeature = new Feature({
|
||||
geometry: lineString
|
||||
});
|
||||
vectorSource.addFeature(lineFeature);
|
||||
const vectorLayer = new Vector({
|
||||
source: vectorSource,
|
||||
style: function(feature) {
|
||||
if (feature.getGeometry().getType() === 'Point') {
|
||||
return new Style({
|
||||
image: new Circle({
|
||||
radius: 10,
|
||||
fill: new Fill({ color: feature.get("last") ? '#dc3545':'#0d6efd' }),
|
||||
stroke: new Stroke({ color: 'white', width: 5 }),
|
||||
})
|
||||
});
|
||||
} else if (feature.getGeometry().getType() === 'LineString') {
|
||||
return new Style({
|
||||
stroke: new Stroke({
|
||||
color: '#0d6efd',
|
||||
width: 2
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
map.addLayer(vectorLayer);
|
||||
}).catch(e => {
|
||||
this.osmAvailable = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="map" class="w-100 rounded-3" v-if="this.osmAvailable"></div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.ol-layer canvas{
|
||||
border-radius: var(--bs-border-radius-lg) !important;
|
||||
}
|
||||
|
||||
#map{
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script>
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export default {
|
||||
name: "message",
|
||||
methods: {
|
||||
dayjs,
|
||||
hide(){
|
||||
this.ct()
|
||||
this.message.show = false;
|
||||
},
|
||||
show(){
|
||||
this.timeout = setTimeout(() => {
|
||||
this.message.show = false
|
||||
}, 5000)
|
||||
},
|
||||
ct(){
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
},
|
||||
components: {LocaleText},
|
||||
props: {
|
||||
message: Object
|
||||
},
|
||||
mounted() {
|
||||
this.show();
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
dismiss: false,
|
||||
timeout: null
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
@mouseenter="dismiss = true; this.ct()"
|
||||
@mouseleave="dismiss = false; this.show()"
|
||||
class="card shadow rounded-3 position-relative message ms-auto"
|
||||
:id="this.message.id">
|
||||
<div
|
||||
:class="{
|
||||
'text-bg-danger': this.message.type === 'danger',
|
||||
'text-bg-success': this.message.type === 'success',
|
||||
'text-bg-warning': this.message.type === 'warning'}"
|
||||
class="card-header pos">
|
||||
<Transition name="zoom" mode="out-in">
|
||||
<div class="d-flex" v-if="!dismiss">
|
||||
<small class="fw-bold d-block" style="text-transform: uppercase">
|
||||
<LocaleText t="FROM "></LocaleText>
|
||||
{{this.message.from}}
|
||||
</small>
|
||||
<small class="ms-auto">
|
||||
{{dayjs().format("hh:mm A")}}
|
||||
</small>
|
||||
</div>
|
||||
<div v-else>
|
||||
<small
|
||||
@click="hide()"
|
||||
class="d-block mx-auto w-100 text-center" style="cursor: pointer">
|
||||
<i class="bi bi-x-lg me-2"></i><LocaleText t="Dismiss"></LocaleText>
|
||||
</small>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center gap-3">
|
||||
<div>
|
||||
{{this.message.content}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.message{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 576px) {
|
||||
.message{
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<script>
|
||||
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
|
||||
import {DashboardConfigurationStore} from "@/stores/DashboardConfigurationStore.js";
|
||||
import {fetchGet} from "@/utilities/fetch.js";
|
||||
import LocaleText from "@/components/text/localeText.vue";
|
||||
import {GetLocale} from "@/utilities/locale.js";
|
||||
import HelpModal from "@/components/navbarComponents/helpModal.vue";
|
||||
import AgentModal from "@/components/navbarComponents/agentModal.vue";
|
||||
|
||||
export default {
|
||||
name: "navbar",
|
||||
components: {HelpModal, LocaleText, AgentModal},
|
||||
setup(){
|
||||
const wireguardConfigurationsStore = WireguardConfigurationsStore();
|
||||
const dashboardConfigurationStore = DashboardConfigurationStore();
|
||||
return {wireguardConfigurationsStore, dashboardConfigurationStore}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
updateAvailable: false,
|
||||
updateMessage: "Checking for update...",
|
||||
updateUrl: "",
|
||||
openHelpModal: false,
|
||||
openAgentModal: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getActiveCrossServer(){
|
||||
if (this.dashboardConfigurationStore.ActiveServerConfiguration){
|
||||
return new URL(this.dashboardConfigurationStore.CrossServerConfiguration.ServerList
|
||||
[this.dashboardConfigurationStore.ActiveServerConfiguration].host)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.wireguardConfigurationsStore.getConfigurations();
|
||||
await fetchGet("/api/getDashboardUpdate", {}, (res) => {
|
||||
if (res.status){
|
||||
if (res.data){
|
||||
this.updateAvailable = true
|
||||
this.updateUrl = res.data
|
||||
}
|
||||
this.updateMessage = res.message
|
||||
}else{
|
||||
this.updateMessage = GetLocale("Failed to check available update")
|
||||
console.log(`Failed to get update: ${res.message}`)
|
||||
}
|
||||
})
|
||||
this.wireguardConfigurationsStore.ConfigurationListInterval = setInterval(() => {
|
||||
this.wireguardConfigurationsStore.getConfigurations()
|
||||
}, 10000)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="col-md-3 col-lg-2 d-md-block p-2 navbar-container bg-transparent"
|
||||
:class="{active: this.dashboardConfigurationStore.ShowNavBar}"
|
||||
:data-bs-theme="dashboardConfigurationStore.Configuration.Server.dashboard_theme"
|
||||
>
|
||||
<nav id="sidebarMenu" class=" bg-body-tertiary sidebar border h-100 rounded-3 shadow overflow-y-scroll" >
|
||||
<div class="sidebar-sticky ">
|
||||
<div class="text-white text-center m-0 py-3 mb-2 btn-brand">
|
||||
<h5 class="mb-0">
|
||||
WGDashboard
|
||||
</h5>
|
||||
<small class="ms-auto" v-if="getActiveCrossServer !== undefined">
|
||||
<i class="bi bi-hdd-rack-fill me-2"></i>{{getActiveCrossServer.host}}
|
||||
</small>
|
||||
</div>
|
||||
<ul class="nav flex-column px-2 gap-1">
|
||||
<li class="nav-item">
|
||||
<RouterLink class="nav-link rounded-3"
|
||||
to="/" exact-active-class="active">
|
||||
<i class="bi bi-house me-2"></i>
|
||||
<LocaleText t="Home"></LocaleText>
|
||||
</RouterLink></li>
|
||||
<li class="nav-item">
|
||||
<RouterLink class="nav-link rounded-3" to="/settings"
|
||||
active-class="active">
|
||||
<i class="bi bi-gear me-2"></i>
|
||||
<LocaleText t="Settings"></LocaleText>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<RouterLink class="nav-link rounded-3" to="/clients"
|
||||
active-class="active">
|
||||
<i class="bi bi-people me-2"></i>
|
||||
<LocaleText t="Clients"></LocaleText>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<RouterLink class="nav-link rounded-3" to="/webhooks"
|
||||
active-class="active">
|
||||
<i class="bi bi-postcard me-2"></i>
|
||||
<LocaleText t="Webhooks"></LocaleText>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link rounded-3" role="button" @click="openAgentModal = true">
|
||||
<i class="bi bi-question-circle me-2"></i>
|
||||
<LocaleText t="Help"></LocaleText>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr class="text-body my-2">
|
||||
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted text-center">
|
||||
<LocaleText t="WireGuard Configurations"></LocaleText>
|
||||
</h6>
|
||||
<ul class="nav flex-column px-2 gap-1">
|
||||
<li class="nav-item" v-for="c in this.wireguardConfigurationsStore.sortConfigurations">
|
||||
<RouterLink :to="'/configuration/'+c.Name + '/peers'" class="nav-link nav-conf-link rounded-3"
|
||||
active-class="active"
|
||||
>
|
||||
<span class="dot me-2" :class="{active: c.Status}"></span>
|
||||
{{c.Name}}
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<hr class="text-body my-2">
|
||||
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted text-center">
|
||||
<LocaleText t="Tools"></LocaleText>
|
||||
</h6>
|
||||
<ul class="nav flex-column px-2 gap-1">
|
||||
<li class="nav-item">
|
||||
<RouterLink to="/system_status" class="nav-link rounded-3" active-class="active">
|
||||
<LocaleText t="System Status"></LocaleText>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<RouterLink to="/ping" class="nav-link rounded-3" active-class="active">
|
||||
<LocaleText t="Ping"></LocaleText>
|
||||
</RouterLink></li>
|
||||
<li class="nav-item">
|
||||
<RouterLink to="/traceroute" class="nav-link rounded-3" active-class="active">
|
||||
<LocaleText t="Traceroute"></LocaleText>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<hr class="text-body my-2">
|
||||
<ul class="nav flex-column px-2 mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-danger rounded-3"
|
||||
@click="this.dashboardConfigurationStore.signOut()"
|
||||
role="button" style="font-weight: bold">
|
||||
<i class="bi bi-box-arrow-left me-2"></i>
|
||||
<LocaleText t="Sign Out"></LocaleText>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" style="font-size: 0.8rem">
|
||||
<a :href="this.updateUrl" v-if="this.updateAvailable" class="text-decoration-none rounded-3" target="_blank">
|
||||
<small class="nav-link text-muted rounded-3" >
|
||||
<LocaleText :t="this.updateMessage"></LocaleText>
|
||||
(<LocaleText t="Current Version:"></LocaleText> {{ dashboardConfigurationStore.Configuration.Server.version }})
|
||||
</small>
|
||||
</a>
|
||||
<small class="nav-link text-muted rounded-3" v-else>
|
||||
<LocaleText :t="this.updateMessage"></LocaleText>
|
||||
({{ dashboardConfigurationStore.Configuration.Server.version }})
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<Transition name="zoom">
|
||||
<HelpModal v-if="this.openHelpModal" @close="openHelpModal = false;"></HelpModal>
|
||||
</Transition>
|
||||
<Transition name="slideIn">
|
||||
<AgentModal v-if="this.openAgentModal" @close="openAgentModal = false"></AgentModal>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (max-width: 768px) {
|
||||
.navbar-container{
|
||||
position: absolute !important;
|
||||
z-index: 1000;
|
||||
animation-duration: 0.4s;
|
||||
animation-fill-mode: both;
|
||||
display: none;
|
||||
animation-timing-function: cubic-bezier(0.82, 0.58, 0.17, 0.9);
|
||||
}
|
||||
|
||||
.navbar-container.active{
|
||||
animation-direction: normal;
|
||||
display: block !important;
|
||||
animation-name: zoomInFade
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-container{
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@supports (height: 100dvh) {
|
||||
@media screen and (max-width: 768px){
|
||||
.navbar-container{
|
||||
height: calc(100dvh - 58px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@keyframes zoomInFade {
|
||||
0%{
|
||||
opacity: 0;
|
||||
transform: translateY(60px);
|
||||
filter: blur(3px);
|
||||
}
|
||||
100%{
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
filter: blur(0px);
|
||||
}
|
||||
}
|
||||
|
||||
.slideIn-enter-active,
|
||||
.slideIn-leave-active{
|
||||
transition: all 0.3s cubic-bezier(0.82, 0.58, 0.17, 1);
|
||||
}
|
||||
|
||||
.slideIn-enter-from,
|
||||
.slideIn-leave-to {
|
||||
transform: translateY(30px);
|
||||
filter: blur(3px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||