chore: add in the WGDashboard assets

This commit is contained in:
DaanSelen
2026-03-03 16:52:35 +01:00
parent 9687c11db6
commit 9e69a066b5
274 changed files with 45296 additions and 12 deletions
+3
View File
@@ -1,4 +1,7 @@
*.db
dist
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
+1 -1
View File
@@ -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
+5 -1
View File
@@ -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
+16 -10
View File
@@ -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"])
+30
View File
@@ -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/*
+27
View File
@@ -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
+26
View File
@@ -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>
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
+9098
View File
File diff suppressed because it is too large Load Diff
+49
View File
@@ -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"
}
}
+1
View File
@@ -0,0 +1 @@
export const proxy = "http://wg.local:10086/"
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

+48
View File
@@ -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"
}
]
}
+75
View File
@@ -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 didnt 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}} &#x2022;
</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>
+130
View File
@@ -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>
+234
View File
@@ -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>

Some files were not shown because too many files have changed in this diff Show More