mirror of
https://github.com/h44z/wg-portal.git
synced 2025-09-13 14:31:15 +00:00
V2 alpha - initial version (#172)
Initial alpha codebase for version 2 of WireGuard Portal. This version is considered unstable and incomplete (for example, no public REST API)! Use with care! Fixes/Implements the following issues: - OAuth support #154, #1 - New Web UI with internationalisation support #98, #107, #89, #62 - Postgres Support #49 - Improved Email handling #47, #119 - DNS Search Domain support #46 - Bugfixes #94, #48 --------- Co-authored-by: Fabian Wechselberger <wechselbergerf@hotmail.com>
This commit is contained in:
1
frontend/.env.development
Normal file
1
frontend/.env.development
Normal file
@@ -0,0 +1 @@
|
||||
VITE_SOME_EXAMPLE_VAR=http://localhost:5000 (can be used internally like: import.meta.env.VITE_SOME_EXAMPLE_VAR)
|
1
frontend/.env.production
Normal file
1
frontend/.env.production
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL=https://wgportal.server.com
|
28
frontend/.gitignore
vendored
Normal file
28
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/extensions.json
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
3
frontend/.vscode/extensions.json
vendored
Normal file
3
frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"]
|
||||
}
|
29
frontend/README.md
Normal file
29
frontend/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# frontend
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
35
frontend/index.html
Normal file
35
frontend/index.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link href="/favicon.ico" rel="icon" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<title>WireGuard Portal</title>
|
||||
<meta content="WireGuard VPN Management Portal" name="description">
|
||||
<script>
|
||||
// global config, will be overridden by backend if available
|
||||
let WGPORTAL_BACKEND_BASE_URL="http://localhost:5000/api/v0";
|
||||
let WGPORTAL_VERSION="unknown";
|
||||
let WGPORTAL_SITE_TITLE="WireGuard Portal";
|
||||
let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal";
|
||||
</script>
|
||||
<script src="/api/v0/config/frontend.js"></script>
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<noscript>
|
||||
<strong>We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
|
||||
<!-- vue teleport will add toasts here -->
|
||||
<div id="toasts"></div>
|
||||
|
||||
<!-- main application -->
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- vue teleport will add modals and dialogs here -->
|
||||
<div id="modals"></div>
|
||||
<div id="dialogs"></div>
|
||||
|
||||
<script src="/src/main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
1684
frontend/package-lock.json
generated
Normal file
1684
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
frontend/package.json
Normal file
31
frontend/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build-dev": "vite build --mode development --base=/app/",
|
||||
"build": "vite build --base=/app/",
|
||||
"preview": "vite preview --port 5050"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"@kyvg/vue3-notification": "^2.9.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.0",
|
||||
"bootswatch": "^5.3.0",
|
||||
"flag-icons": "^6.7.0",
|
||||
"is-cidr": "^5.0.3",
|
||||
"is-ip": "^5.0.0",
|
||||
"pinia": "^2.1.4",
|
||||
"prismjs": "^1.29.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-prism-component": "github:h44z/vue-prism-component",
|
||||
"vue-router": "^4.2.2",
|
||||
"vue3-tags-input": "^1.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
}
|
BIN
frontend/public/favicon-large.png
Normal file
BIN
frontend/public/favicon-large.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 99 KiB |
BIN
frontend/public/favicon.png
Normal file
BIN
frontend/public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
BIN
frontend/public/img/header-logo.png
Normal file
BIN
frontend/public/img/header-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
128
frontend/src/App.vue
Normal file
128
frontend/src/App.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<script setup>
|
||||
import { RouterLink, RouterView } from 'vue-router';
|
||||
import {computed, getCurrentInstance, onMounted, ref} from "vue";
|
||||
import {authStore} from "./stores/auth";
|
||||
import {securityStore} from "./stores/security";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
|
||||
const appGlobal = getCurrentInstance().appContext.config.globalProperties
|
||||
const auth = authStore()
|
||||
const sec = securityStore()
|
||||
const settings = settingsStore()
|
||||
|
||||
onMounted(async () => {
|
||||
console.log("Starting WireGuard Portal frontend...");
|
||||
|
||||
await sec.LoadSecurityProperties();
|
||||
await auth.LoadProviders();
|
||||
|
||||
let wasLoggedIn = auth.IsAuthenticated;
|
||||
try {
|
||||
await auth.LoadSession();
|
||||
await settings.LoadSettings(); // only logs errors, does not throw
|
||||
|
||||
console.log("WireGuard Portal session is valid");
|
||||
} catch (e) {
|
||||
if (wasLoggedIn) {
|
||||
console.log("WireGuard Portal invalid - logging out");
|
||||
await auth.Logout();
|
||||
}
|
||||
}
|
||||
|
||||
console.log("WireGuard Portal ready!");
|
||||
})
|
||||
|
||||
const switchLanguage = function (lang) {
|
||||
if (appGlobal.$i18n.locale !== lang) {
|
||||
localStorage.setItem('wgLang', lang);
|
||||
appGlobal.$i18n.locale = lang;
|
||||
}
|
||||
}
|
||||
|
||||
const languageFlag = computed(() => {
|
||||
// `this` points to the component instance
|
||||
let lang = appGlobal.$i18n.locale.toLowerCase();
|
||||
if (lang === "en") {
|
||||
lang = "us";
|
||||
}
|
||||
return "fi-" + lang;
|
||||
})
|
||||
|
||||
const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
||||
const wgVersion = ref(WGPORTAL_VERSION);
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<notifications :duration="3000" :ignore-duplicates="true" position="top right" />
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><img alt="WireGuard Portal" src="/img/header-logo.png" /></a>
|
||||
<button aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
|
||||
data-bs-target="#navbarTop" data-bs-toggle="collapse" type="button">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div id="navbarTop" class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<RouterLink :to="{ name: 'home' }" class="nav-link">{{ $t('menu.home') }}</RouterLink>
|
||||
</li>
|
||||
<li v-if="auth.IsAuthenticated && auth.IsAdmin" class="nav-item">
|
||||
<RouterLink :to="{ name: 'interfaces' }" class="nav-link">{{ $t('menu.interfaces') }}</RouterLink>
|
||||
</li>
|
||||
<li v-if="auth.IsAuthenticated && auth.IsAdmin" class="nav-item">
|
||||
<RouterLink :to="{ name: 'users' }" class="nav-link">{{ $t('menu.users') }}</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="navbar-nav d-flex justify-content-end">
|
||||
<div v-if="auth.IsAuthenticated" class="nav-item dropdown">
|
||||
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#"
|
||||
role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
||||
<div class="dropdown-menu">
|
||||
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" @click.prevent="auth.Logout">
|
||||
<i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!auth.IsAuthenticated" class="nav-item">
|
||||
<RouterLink :to="{ name: 'login' }" class="nav-link">
|
||||
<i class="fas fa-sign-in-alt fa-sm fa-fw me-2"></i>{{ $t('menu.login') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-5 flex-shrink-0">
|
||||
<RouterView />
|
||||
</div>
|
||||
|
||||
<footer class="page-footer mt-auto">
|
||||
<div class="container mt-5">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-6">Copyright © {{ companyName }} {{ currentYear }} <span v-if="auth.IsAuthenticated"> - version {{ wgVersion }}</span></div>
|
||||
<div class="col-6 text-end">
|
||||
<div :aria-label="$t('menu.lang')" class="btn-group" role="group">
|
||||
<div class="btn-group" role="group">
|
||||
<button aria-expanded="false" aria-haspopup="true" class="btn btn btn-secondary pe-0" data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button>
|
||||
<div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style="">
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
5
frontend/src/assets/base.css
Normal file
5
frontend/src/assets/base.css
Normal file
@@ -0,0 +1,5 @@
|
||||
a.disabled {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
color: #888888;
|
||||
}
|
1
frontend/src/assets/logo.svg
Normal file
1
frontend/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 308 B |
54
frontend/src/components/Confirmation.vue
Normal file
54
frontend/src/components/Confirmation.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const title = ref("Default Title")
|
||||
const question = ref("Default Question")
|
||||
const visible = ref(true)
|
||||
|
||||
const emit = defineEmits(['no', 'yes'])
|
||||
|
||||
function showDialog(titleStr, questionStr) {
|
||||
visible.value = true
|
||||
title.value = titleStr
|
||||
question.value = questionStr
|
||||
}
|
||||
|
||||
function yes() {
|
||||
visible.value = false
|
||||
emit('yes')
|
||||
}
|
||||
|
||||
function no() {
|
||||
visible.value = false
|
||||
emit('no')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="#dialogs">
|
||||
<div v-if="visible" class="modal-backdrop fade show">
|
||||
<div class="modal fade show" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-scrollable" @click.stop="">
|
||||
<div class="modal-content" ref="body">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ title }}</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ question }}
|
||||
</div>
|
||||
<div class="modal-footer pt-0 border-top-0">
|
||||
<button type="button" class="btn btn-primary" @click="no">{{ $t('general.no') }}</button>
|
||||
<button type="button" class="btn btn-success" @click="yes">{{ $t('general.yes') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
513
frontend/src/components/InterfaceEditModal.vue
Normal file
513
frontend/src/components/InterfaceEditModal.vue
Normal file
@@ -0,0 +1,513 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import Vue3TagsInput from 'vue3-tags-input';
|
||||
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
||||
import isCidr from "is-cidr";
|
||||
import {isIP} from 'is-ip';
|
||||
import { freshInterface } from '@/helpers/models';
|
||||
import {peerStore} from "@/stores/peers";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const interfaces = interfaceStore()
|
||||
const peers = peerStore()
|
||||
|
||||
const props = defineProps({
|
||||
interfaceId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
return interfaces.Find(props.interfaceId)
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
|
||||
if (selectedInterface.value) {
|
||||
return t("modals.interface-edit.headline-edit") + " " + selectedInterface.value.Identifier
|
||||
}
|
||||
return t("modals.interface-edit.headline-new")
|
||||
})
|
||||
|
||||
const formData = ref(freshInterface())
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
console.log(selectedInterface.value)
|
||||
if (!selectedInterface.value) {
|
||||
await interfaces.PrepareInterface()
|
||||
|
||||
// fill form data
|
||||
formData.value.Identifier = interfaces.Prepared.Identifier
|
||||
formData.value.DisplayName = interfaces.Prepared.DisplayName
|
||||
formData.value.Mode = interfaces.Prepared.Mode
|
||||
|
||||
formData.value.PublicKey = interfaces.Prepared.PublicKey
|
||||
formData.value.PrivateKey = interfaces.Prepared.PrivateKey
|
||||
|
||||
formData.value.ListenPort = interfaces.Prepared.ListenPort
|
||||
formData.value.Addresses = interfaces.Prepared.Addresses
|
||||
formData.value.Dns = interfaces.Prepared.Dns
|
||||
formData.value.DnsSearch = interfaces.Prepared.DnsSearch
|
||||
|
||||
formData.value.Mtu = interfaces.Prepared.Mtu
|
||||
formData.value.FirewallMark = interfaces.Prepared.FirewallMark
|
||||
formData.value.RoutingTable = interfaces.Prepared.RoutingTable
|
||||
|
||||
formData.value.PreUp = interfaces.Prepared.PreUp
|
||||
formData.value.PostUp = interfaces.Prepared.PostUp
|
||||
formData.value.PreDown = interfaces.Prepared.PreDown
|
||||
formData.value.PostDown = interfaces.Prepared.PostDown
|
||||
|
||||
formData.value.SaveConfig = interfaces.Prepared.SaveConfig
|
||||
|
||||
formData.value.PeerDefNetwork = interfaces.Prepared.PeerDefNetwork
|
||||
formData.value.PeerDefDns = interfaces.Prepared.PeerDefDns
|
||||
formData.value.PeerDefDnsSearch = interfaces.Prepared.PeerDefDnsSearch
|
||||
formData.value.PeerDefEndpoint = interfaces.Prepared.PeerDefEndpoint
|
||||
formData.value.PeerDefAllowedIPs = interfaces.Prepared.PeerDefAllowedIPs
|
||||
formData.value.PeerDefMtu = interfaces.Prepared.PeerDefMtu
|
||||
formData.value.PeerDefPersistentKeepalive = interfaces.Prepared.PeerDefPersistentKeepalive
|
||||
formData.value.PeerDefFirewallMark = interfaces.Prepared.PeerDefFirewallMark
|
||||
formData.value.PeerDefRoutingTable = interfaces.Prepared.PeerDefRoutingTable
|
||||
formData.value.PeerDefPreUp = interfaces.Prepared.PeerDefPreUp
|
||||
formData.value.PeerDefPostUp = interfaces.Prepared.PeerDefPostUp
|
||||
formData.value.PeerDefPreDown = interfaces.Prepared.PeerDefPreDown
|
||||
formData.value.PeerDefPostDown = interfaces.Prepared.PeerDefPostDown
|
||||
} else { // fill existing userdata
|
||||
formData.value.Disabled = selectedInterface.value.Disabled
|
||||
formData.value.Identifier = selectedInterface.value.Identifier
|
||||
formData.value.DisplayName = selectedInterface.value.DisplayName
|
||||
formData.value.Mode = selectedInterface.value.Mode
|
||||
|
||||
formData.value.PublicKey = selectedInterface.value.PublicKey
|
||||
formData.value.PrivateKey = selectedInterface.value.PrivateKey
|
||||
|
||||
formData.value.ListenPort = selectedInterface.value.ListenPort
|
||||
formData.value.Addresses = selectedInterface.value.Addresses
|
||||
formData.value.Dns = selectedInterface.value.Dns
|
||||
formData.value.DnsSearch = selectedInterface.value.DnsSearch
|
||||
|
||||
formData.value.Mtu = selectedInterface.value.Mtu
|
||||
formData.value.FirewallMark = selectedInterface.value.FirewallMark
|
||||
formData.value.RoutingTable = selectedInterface.value.RoutingTable
|
||||
|
||||
formData.value.PreUp = selectedInterface.value.PreUp
|
||||
formData.value.PostUp = selectedInterface.value.PostUp
|
||||
formData.value.PreDown = selectedInterface.value.PreDown
|
||||
formData.value.PostDown = selectedInterface.value.PostDown
|
||||
|
||||
formData.value.SaveConfig = selectedInterface.value.SaveConfig
|
||||
|
||||
formData.value.PeerDefNetwork = selectedInterface.value.PeerDefNetwork
|
||||
formData.value.PeerDefDns = selectedInterface.value.PeerDefDns
|
||||
formData.value.PeerDefDnsSearch = selectedInterface.value.PeerDefDnsSearch
|
||||
formData.value.PeerDefEndpoint = selectedInterface.value.PeerDefEndpoint
|
||||
formData.value.PeerDefAllowedIPs = selectedInterface.value.PeerDefAllowedIPs
|
||||
formData.value.PeerDefMtu = selectedInterface.value.PeerDefMtu
|
||||
formData.value.PeerDefPersistentKeepalive = selectedInterface.value.PeerDefPersistentKeepalive
|
||||
formData.value.PeerDefFirewallMark = selectedInterface.value.PeerDefFirewallMark
|
||||
formData.value.PeerDefRoutingTable = selectedInterface.value.PeerDefRoutingTable
|
||||
formData.value.PeerDefPreUp = selectedInterface.value.PeerDefPreUp
|
||||
formData.value.PeerDefPostUp = selectedInterface.value.PeerDefPostUp
|
||||
formData.value.PeerDefPreDown = selectedInterface.value.PeerDefPreDown
|
||||
formData.value.PeerDefPostDown = selectedInterface.value.PeerDefPostDown
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
formData.value = freshInterface()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleChangeAddresses(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Addresses = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDns(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(!isIP(tag)) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid IP",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Dns = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDnsSearch(tags) {
|
||||
formData.value.DnsSearch = tags
|
||||
}
|
||||
|
||||
function handleChangePeerDefNetwork(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.PeerDefNetwork = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePeerDefAllowedIPs(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.PeerDefAllowedIPs = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePeerDefDns(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(!isIP(tag)) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid IP",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.PeerDefDns = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePeerDefDnsSearch(tags) {
|
||||
formData.value.PeerDefDnsSearch = tags
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
if (props.interfaceId!=='#NEW#') {
|
||||
await interfaces.UpdateInterface(selectedInterface.value.Identifier, formData.value)
|
||||
} else {
|
||||
await interfaces.CreateInterface(formData.value)
|
||||
}
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to save interface!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function applyPeerDefaults() {
|
||||
if (props.interfaceId==='#NEW#') {
|
||||
return; // do nothing for new interfaces
|
||||
}
|
||||
|
||||
try {
|
||||
await interfaces.ApplyPeerDefaults(selectedInterface.value.Identifier, formData.value)
|
||||
|
||||
notify({
|
||||
title: "Peer Defaults Applied",
|
||||
text: "Applied current peer defaults to all available peers.",
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
await peers.LoadPeers(selectedInterface.value.Identifier) // reload all peers after applying the defaults
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to apply peer defaults!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
try {
|
||||
await interfaces.DeleteInterface(selectedInterface.value.Identifier)
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to delete interface!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#interface">{{ $t('modals.interface-edit.tab-interface') }}</a>
|
||||
</li>
|
||||
<li v-if="formData.Mode==='server'" class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#peerdefaults">{{ $t('modals.interface-edit.tab-peerdef') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div id="interfaceTabs" class="tab-content">
|
||||
<div id="interface" class="tab-pane fade active show">
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-general') }}</legend>
|
||||
<div v-if="props.interfaceId==='#NEW#'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.identifier.label') }}</label>
|
||||
<input v-model="formData.Identifier" class="form-control" :placeholder="$t('modals.interface-edit.identifier.placeholder')" type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.mode.label') }}</label>
|
||||
<select v-model="formData.Mode" class="form-select">
|
||||
<option value="server">{{ $t('modals.interface-edit.mode.server') }}</option>
|
||||
<option value="client">{{ $t('modals.interface-edit.mode.client') }}</option>
|
||||
<option value="any">{{ $t('modals.interface-edit.mode.any') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.display-name.label') }}</label>
|
||||
<input v-model="formData.DisplayName" class="form-control" :placeholder="$t('modals.interface-edit.display-name.placeholder')" type="text">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-crypto') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.private-key.label') }}</label>
|
||||
<input v-model="formData.PrivateKey" class="form-control" :placeholder="$t('modals.interface-edit.private-key.placeholder')" required type="email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.public-key.label') }}</label>
|
||||
<input v-model="formData.PublicKey" class="form-control" :placeholder="$t('modals.interface-edit.public-key.placeholder')" required type="email">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Addresses"
|
||||
:placeholder="$t('modals.interface-edit.ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeAddresses"/>
|
||||
</div>
|
||||
<div v-if="formData.Mode==='server'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label>
|
||||
<input v-model="formData.ListenPort" class="form-control" :placeholder="$t('modals.interface-edit.listen-port.placeholder')" type="number">
|
||||
</div>
|
||||
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Dns"
|
||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateIP"
|
||||
@on-tags-changed="handleChangeDns"/>
|
||||
</div>
|
||||
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch"
|
||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateDomain"
|
||||
@on-tags-changed="handleChangeDnsSearch"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.mtu.label') }}</label>
|
||||
<input v-model="formData.Mtu" class="form-control" :placeholder="$t('modals.interface-edit.mtu.placeholder')" type="number">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.firewall-mark.label') }}</label>
|
||||
<input v-model="formData.FirewallMark" class="form-control" :placeholder="$t('modals.interface-edit.firewall-mark.placeholder')" type="number">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.routing-table.label') }}</label>
|
||||
<input v-model="formData.RoutingTable" aria-describedby="routingTableHelp" class="form-control" :placeholder="$t('modals.interface-edit.routing-table.placeholder')" type="text">
|
||||
<small id="routingTableHelp" class="form-text text-muted">{{ $t('modals.interface-edit.routing-table.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-hooks') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-up.label') }}</label>
|
||||
<textarea v-model="formData.PreUp" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.pre-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.post-up.label') }}</label>
|
||||
<textarea v-model="formData.PostUp" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.post-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-down.label') }}</label>
|
||||
<textarea v-model="formData.PreDown" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.pre-down.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.post-down.label') }}</label>
|
||||
<textarea v-model="formData.PostDown" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.post-down.placeholder')"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-state') }}</legend>
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="formData.Disabled" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.interface-edit.disabled.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="formData.SaveConfig" checked="" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.interface-edit.save-config.label') }}</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div id="peerdefaults" class="tab-pane fade">
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.endpoint.label') }}</label>
|
||||
<input v-model="formData.PeerDefEndpoint" class="form-control" :placeholder="$t('modals.interface-edit.defaults.endpoint.placeholder')" type="text">
|
||||
<small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.endpoint.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefNetwork"
|
||||
:placeholder="$t('modals.interface-edit.defaults.networks.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangePeerDefNetwork"/>
|
||||
<small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.networks.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowed-ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefAllowedIPs"
|
||||
:placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangePeerDefAllowedIPs"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefDns"
|
||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateIP"
|
||||
@on-tags-changed="handleChangePeerDefDns"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefDnsSearch"
|
||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateDomain"
|
||||
@on-tags-changed="handleChangePeerDefDnsSearch"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.mtu.label') }}</label>
|
||||
<input v-model="formData.PeerDefMtu" class="form-control" :placeholder="$t('modals.interface-edit.defaults.mtu.placeholder')" type="number">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.firewall-mark.label') }}</label>
|
||||
<input v-model="formData.PeerDefFirewallMark" class="form-control" :placeholder="$t('modals.interface-edit.firewall-mark.placeholder')" type="number">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.routing-table.label') }}</label>
|
||||
<input v-model="formData.PeerDefRoutingTable" class="form-control" :placeholder="$t('modals.interface-edit.routing-table.placeholder')" type="number">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.keep-alive.label') }}</label>
|
||||
<input v-model="formData.PeerDefPersistentKeepalive" class="form-control" :placeholder="$t('modals.interface-edit.defaults.keep-alive.placeholder')" type="number">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-peer-hooks') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-up.label') }}</label>
|
||||
<textarea v-model="formData.PeerDefPreUp" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.pre-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.post-up.label') }}</label>
|
||||
<textarea v-model="formData.PeerDefPostUp" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.post-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-down.label') }}</label>
|
||||
<textarea v-model="formData.PeerDefPreDown" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.pre-down.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.post-down.label') }}</label>
|
||||
<textarea v-model="formData.PeerDefPostDown" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.post-down.placeholder')"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset v-if="props.interfaceId!=='#NEW#'" class="text-end">
|
||||
<hr class="mt-4">
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="applyPeerDefaults">{{ $t('modals.interface-edit.button-apply-defaults') }}</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.interfaceId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
</div>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
60
frontend/src/components/InterfaceViewModal.vue
Normal file
60
frontend/src/components/InterfaceViewModal.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import Prism from 'vue-prism-component'
|
||||
import 'prismjs/components/prism-ini'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const interfaces = interfaceStore()
|
||||
|
||||
const props = defineProps({
|
||||
interfaceId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const configString = ref("")
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
return interfaces.Find(props.interfaceId)
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
|
||||
return t("modals.interface-view.headline") + " " + selectedInterface.value.Identifier
|
||||
})
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
console.log(selectedInterface.value)
|
||||
await interfaces.LoadInterfaceConfig(selectedInterface.value.Identifier)
|
||||
configString.value = interfaces.configuration
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<Prism language="ini" :code="configString"></Prism>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="btn btn-primary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
59
frontend/src/components/Modal.vue
Normal file
59
frontend/src/components/Modal.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<Teleport to="#modals">
|
||||
<div v-show="visible" class="modal-backdrop fade show" @click="closeBackdrop">
|
||||
<div class="modal fade show" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable" @click.stop="">
|
||||
<div class="modal-content" ref="body">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ title }}</h5>
|
||||
<button @click="closeModal" class="btn-close" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body col-md-12">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.modal.show {
|
||||
display:block;
|
||||
}
|
||||
.modal.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.modal-backdrop {
|
||||
background-color: rgba(0,0,0,0.6) !important;
|
||||
}
|
||||
.modal-backdrop.show {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
visible: Boolean,
|
||||
closeOnBackdrop: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
function closeBackdrop() {
|
||||
if(props.closeOnBackdrop) {
|
||||
console.log("CLOSING BD")
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
console.log("CLOSING")
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
431
frontend/src/components/PeerEditModal.vue
Normal file
431
frontend/src/components/PeerEditModal.vue
Normal file
@@ -0,0 +1,431 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {peerStore} from "@/stores/peers";
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import Vue3TagsInput from "vue3-tags-input";
|
||||
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
||||
import isCidr from "is-cidr";
|
||||
import {isIP} from 'is-ip';
|
||||
import { freshPeer, freshInterface } from '@/helpers/models';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const peers = peerStore()
|
||||
const interfaces = interfaceStore()
|
||||
|
||||
const props = defineProps({
|
||||
peerId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedPeer = computed(() => {
|
||||
return peers.Find(props.peerId)
|
||||
})
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
let i = interfaces.GetSelected;
|
||||
|
||||
if (!i) {
|
||||
i = freshInterface() // dummy interface to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
if (selectedInterface.value.Mode === "server") {
|
||||
if (selectedPeer.value) {
|
||||
return t("modals.peer-edit.headline-edit-peer") + " " + selectedPeer.value.Identifier
|
||||
}
|
||||
return t("modals.peer-edit.headline-new-peer")
|
||||
} else {
|
||||
if (selectedPeer.value) {
|
||||
return t("modals.peer-edit.headline-edit-endpoint") + " " + selectedPeer.value.Identifier
|
||||
}
|
||||
return t("modals.peer-edit.headline-new-endpoint")
|
||||
}
|
||||
})
|
||||
|
||||
const formData = ref(freshPeer())
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
console.log(selectedInterface.value)
|
||||
console.log(selectedPeer.value)
|
||||
if (!selectedPeer.value) {
|
||||
await peers.PreparePeer(selectedInterface.value.Identifier)
|
||||
|
||||
formData.value.Identifier = peers.Prepared.Identifier
|
||||
formData.value.DisplayName = peers.Prepared.DisplayName
|
||||
formData.value.UserIdentifier = peers.Prepared.UserIdentifier
|
||||
formData.value.InterfaceIdentifier = peers.Prepared.InterfaceIdentifier
|
||||
formData.value.Disabled = peers.Prepared.Disabled
|
||||
formData.value.ExpiresAt = peers.Prepared.ExpiresAt
|
||||
formData.value.Notes = peers.Prepared.Notes
|
||||
|
||||
formData.value.Endpoint = peers.Prepared.Endpoint
|
||||
formData.value.EndpointPublicKey = peers.Prepared.EndpointPublicKey
|
||||
formData.value.AllowedIPs = peers.Prepared.AllowedIPs
|
||||
formData.value.ExtraAllowedIPs = peers.Prepared.ExtraAllowedIPs
|
||||
formData.value.PresharedKey = peers.Prepared.PresharedKey
|
||||
formData.value.PersistentKeepalive = peers.Prepared.PersistentKeepalive
|
||||
|
||||
formData.value.PrivateKey = peers.Prepared.PrivateKey
|
||||
formData.value.PublicKey = peers.Prepared.PublicKey
|
||||
|
||||
formData.value.Mode = peers.Prepared.Mode
|
||||
|
||||
formData.value.Addresses = peers.Prepared.Addresses
|
||||
formData.value.CheckAliveAddress = peers.Prepared.CheckAliveAddress
|
||||
formData.value.Dns = peers.Prepared.Dns
|
||||
formData.value.DnsSearch = peers.Prepared.DnsSearch
|
||||
formData.value.Mtu = peers.Prepared.Mtu
|
||||
formData.value.FirewallMark = peers.Prepared.FirewallMark
|
||||
formData.value.RoutingTable = peers.Prepared.RoutingTable
|
||||
|
||||
formData.value.PreUp = peers.Prepared.PreUp
|
||||
formData.value.PostUp = peers.Prepared.PostUp
|
||||
formData.value.PreDown = peers.Prepared.PreDown
|
||||
formData.value.PostDown = peers.Prepared.PostDown
|
||||
|
||||
} else { // fill existing data
|
||||
formData.value.Identifier = selectedPeer.value.Identifier
|
||||
formData.value.DisplayName = selectedPeer.value.DisplayName
|
||||
formData.value.UserIdentifier = selectedPeer.value.UserIdentifier
|
||||
formData.value.InterfaceIdentifier = selectedPeer.value.InterfaceIdentifier
|
||||
formData.value.Disabled = selectedPeer.value.Disabled
|
||||
formData.value.ExpiresAt = selectedPeer.value.ExpiresAt
|
||||
formData.value.Notes = selectedPeer.value.Notes
|
||||
|
||||
formData.value.Endpoint = selectedPeer.value.Endpoint
|
||||
formData.value.EndpointPublicKey = selectedPeer.value.EndpointPublicKey
|
||||
formData.value.AllowedIPs = selectedPeer.value.AllowedIPs
|
||||
formData.value.ExtraAllowedIPs = selectedPeer.value.ExtraAllowedIPs
|
||||
formData.value.PresharedKey = selectedPeer.value.PresharedKey
|
||||
formData.value.PersistentKeepalive = selectedPeer.value.PersistentKeepalive
|
||||
|
||||
formData.value.PrivateKey = selectedPeer.value.PrivateKey
|
||||
formData.value.PublicKey = selectedPeer.value.PublicKey
|
||||
|
||||
formData.value.Mode = selectedPeer.value.Mode
|
||||
|
||||
formData.value.Addresses = selectedPeer.value.Addresses
|
||||
formData.value.CheckAliveAddress = selectedPeer.value.CheckAliveAddress
|
||||
formData.value.Dns = selectedPeer.value.Dns
|
||||
formData.value.DnsSearch = selectedPeer.value.DnsSearch
|
||||
formData.value.Mtu = selectedPeer.value.Mtu
|
||||
formData.value.FirewallMark = selectedPeer.value.FirewallMark
|
||||
formData.value.RoutingTable = selectedPeer.value.RoutingTable
|
||||
|
||||
formData.value.PreUp = selectedPeer.value.PreUp
|
||||
formData.value.PostUp = selectedPeer.value.PostUp
|
||||
formData.value.PreDown = selectedPeer.value.PreDown
|
||||
formData.value.PostDown = selectedPeer.value.PostDown
|
||||
|
||||
if (!formData.value.Endpoint.Overridable ||
|
||||
!formData.value.EndpointPublicKey.Overridable ||
|
||||
!formData.value.AllowedIPs.Overridable ||
|
||||
!formData.value.PersistentKeepalive.Overridable ||
|
||||
!formData.value.Dns.Overridable ||
|
||||
!formData.value.DnsSearch.Overridable ||
|
||||
!formData.value.Mtu.Overridable ||
|
||||
!formData.value.FirewallMark.Overridable ||
|
||||
!formData.value.RoutingTable.Overridable ||
|
||||
!formData.value.PreUp.Overridable ||
|
||||
!formData.value.PostUp.Overridable ||
|
||||
!formData.value.PreDown.Overridable ||
|
||||
!formData.value.PostDown.Overridable) {
|
||||
formData.value.IgnoreGlobalSettings = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(() => formData.value.IgnoreGlobalSettings, async (newValue, oldValue) => {
|
||||
formData.value.Endpoint.Overridable = !newValue
|
||||
formData.value.EndpointPublicKey.Overridable = !newValue
|
||||
formData.value.AllowedIPs.Overridable = !newValue
|
||||
formData.value.PersistentKeepalive.Overridable = !newValue
|
||||
formData.value.Dns.Overridable = !newValue
|
||||
formData.value.DnsSearch.Overridable = !newValue
|
||||
formData.value.Mtu.Overridable = !newValue
|
||||
formData.value.FirewallMark.Overridable = !newValue
|
||||
formData.value.RoutingTable.Overridable = !newValue
|
||||
formData.value.PreUp.Overridable = !newValue
|
||||
formData.value.PostUp.Overridable = !newValue
|
||||
formData.value.PreDown.Overridable = !newValue
|
||||
formData.value.PostDown.Overridable = !newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(() => formData.value.Disabled, async (newValue, oldValue) => {
|
||||
if (oldValue && !newValue && formData.value.ExpiresAt) {
|
||||
formData.value.ExpiresAt = "" // reset expiry date
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
formData.value = freshPeer()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleChangeAddresses(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Addresses = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeAllowedIPs(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.AllowedIPs.Value = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeExtraAllowedIPs(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.ExtraAllowedIPs = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDns(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(!isIP(tag)) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid IP",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Dns = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDnsSearch(tags) {
|
||||
formData.value.DnsSearch = tags
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
if (props.peerId!=='#NEW#') {
|
||||
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
|
||||
} else {
|
||||
await peers.CreatePeer(selectedInterface.value.Identifier, formData.value)
|
||||
}
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to save peer!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
try {
|
||||
await peers.DeletePeer(selectedPeer.value.Identifier)
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to delete peer!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-general') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.display-name.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.display-name.placeholder')" v-model="formData.DisplayName">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.linked-user.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.linked-user.placeholder')" v-model="formData.UserIdentifier">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
|
||||
<div class="form-group" v-if="selectedInterface.Mode==='server'">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required v-model="formData.PrivateKey">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required v-model="formData.PublicKey">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')" v-model="formData.PresharedKey">
|
||||
</div>
|
||||
<div class="form-group" v-if="formData.Mode==='client'">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.endpoint-public-key.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint-public-key.placeholder')" v-model="formData.EndpointPublicKey.Value">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-network') }}</legend>
|
||||
<div class="form-group" v-if="selectedInterface.Mode==='client'">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.endpoint.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint.placeholder')" v-model="formData.Endpoint.Value">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Addresses"
|
||||
:placeholder="$t('modals.peer-edit.ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeAddresses"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.allowed-ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.AllowedIPs.Value"
|
||||
:placeholder="$t('modals.peer-edit.allowed-ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeAllowedIPs"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.extra-allowed-ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.ExtraAllowedIPs"
|
||||
:placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeExtraAllowedIPs"/>
|
||||
<small class="form-text text-muted">{{ $t('modals.peer-edit.extra-allowed-ip.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Dns.Value"
|
||||
:placeholder="$t('modals.peer-edit.dns.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateIP"
|
||||
@on-tags-changed="handleChangeDns"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns-search.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch.Value"
|
||||
:placeholder="$t('modals.peer-edit.dns-search.label')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateDomain"
|
||||
@on-tags-changed="handleChangeDnsSearch"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.keep-alive.label') }}</label>
|
||||
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.keep-alive.label')" v-model="formData.PersistentKeepalive.Value">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.mtu.label') }}</label>
|
||||
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.mtu.label')" v-model="formData.Mtu.Value">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-hooks') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-up.label') }}</label>
|
||||
<textarea v-model="formData.PreUp.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.pre-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-up.label') }}</label>
|
||||
<textarea v-model="formData.PostUp.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.post-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-down.label') }}</label>
|
||||
<textarea v-model="formData.PreDown.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.pre-down.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-down.label') }}</label>
|
||||
<textarea v-model="formData.PostDown.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.post-down.placeholder')"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-state') }}</legend>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
|
||||
<label class="form-check-label" >{{ $t('modals.peer-edit.disabled.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" v-model="formData.IgnoreGlobalSettings">
|
||||
<label class="form-check-label">{{ $t('modals.peer-edit.ignore-global.label') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label">{{ $t('modals.peer-edit.expires-at.label') }}</label>
|
||||
<input type="date" pattern="\d{4}-\d{2}-\d{2}" class="form-control" min="2023-01-01" v-model="formData.ExpiresAt">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.peerId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
</div>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
110
frontend/src/components/PeerMultiCreateModal.vue
Normal file
110
frontend/src/components/PeerMultiCreateModal.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {peerStore} from "@/stores/peers";
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import Vue3TagsInput from "vue3-tags-input";
|
||||
import { freshInterface } from '@/helpers/models';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const peers = peerStore()
|
||||
const interfaces = interfaceStore()
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
let i = interfaces.GetSelected;
|
||||
|
||||
if (!i) {
|
||||
i = freshInterface() // dummy interface to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
function freshForm() {
|
||||
return {
|
||||
Identifiers: [],
|
||||
Suffix: "",
|
||||
}
|
||||
}
|
||||
|
||||
const formData = ref(freshForm())
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
if (selectedInterface.value.Mode === "server") {
|
||||
return t("modals.peer-multi-create.headline-peer")
|
||||
} else {
|
||||
return t("modals.peer-multi-create.headline-endpoint")
|
||||
}
|
||||
})
|
||||
|
||||
function close() {
|
||||
formData.value = freshForm()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleChangeUserIdentifiers(tags) {
|
||||
formData.value.Identifiers = tags
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (formData.value.Identifiers.length === 0) {
|
||||
notify({
|
||||
title: "Missing Identifiers",
|
||||
text: "At least one identifier is required to create a new peer.",
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await peers.CreateMultiplePeers(selectedInterface.value.Identifier, formData.value)
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to create peers!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.identifiers.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Identifiers"
|
||||
:placeholder="$t('modals.peer-multi-create.identifiers.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
@on-tags-changed="handleChangeUserIdentifiers"/>
|
||||
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.identifiers.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.prefix.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Suffix">
|
||||
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.prefix.description') }}</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
199
frontend/src/components/PeerViewModal.vue
Normal file
199
frontend/src/components/PeerViewModal.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {peerStore} from "@/stores/peers";
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {freshInterface, freshPeer, freshStats} from '@/helpers/models';
|
||||
import Prism from "vue-prism-component";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const settings = settingsStore()
|
||||
const peers = peerStore()
|
||||
const interfaces = interfaceStore()
|
||||
|
||||
const props = defineProps({
|
||||
peerId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const configString = ref("")
|
||||
|
||||
const selectedPeer = computed(() => {
|
||||
let p = peers.Find(props.peerId)
|
||||
|
||||
if (!p) {
|
||||
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
return p
|
||||
})
|
||||
|
||||
const selectedStats = computed(() => {
|
||||
let s = peers.Statistics(props.peerId)
|
||||
|
||||
if (!s) {
|
||||
s = freshStats() // dummy peer to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
let i = interfaces.GetSelected;
|
||||
|
||||
if (!i) {
|
||||
i = freshInterface() // dummy interface to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
if (selectedInterface.value.Mode === "server") {
|
||||
return t("modals.peer-view.headline-peer") + " " + selectedPeer.value.DisplayName
|
||||
} else {
|
||||
return t("modals.peer-view.headline-endpoint") + " " + selectedPeer.value.DisplayName
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
await peers.LoadPeerConfig(selectedPeer.value.Identifier)
|
||||
configString.value = peers.configuration
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function download() {
|
||||
// credit: https://www.bitdegree.org/learn/javascript-download
|
||||
let filename = 'WireGuard-Tunnel.conf'
|
||||
if (selectedPeer.value.DisplayName) {
|
||||
filename = selectedPeer.value.DisplayName
|
||||
.replace(/ /g,"_")
|
||||
.replace(/[^a-zA-Z0-9-_]/g,"")
|
||||
.substring(0, 16)
|
||||
+ ".conf"
|
||||
}
|
||||
let text = configString.value
|
||||
|
||||
let element = document.createElement('a')
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
|
||||
element.setAttribute('download', filename)
|
||||
|
||||
element.style.display = 'none'
|
||||
document.body.appendChild(element)
|
||||
|
||||
element.click()
|
||||
document.body.removeChild(element)
|
||||
}
|
||||
|
||||
function email() {
|
||||
peers.MailPeerConfig(settings.Setting("MailLinkOnly"), [selectedPeer.value.Identifier]).catch(e => {
|
||||
notify({
|
||||
title: "Failed to send mail with peer configuration!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<div class="accordion" id="peerInformation">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDetails" aria-expanded="true" aria-controls="collapseDetails">
|
||||
{{ $t('modals.peer-view.section-info') }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseDetails" class="accordion-collapse collapse show" aria-labelledby="headingDetails" data-bs-parent="#peerInformation" style="">
|
||||
<div class="accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<ul>
|
||||
<li>{{ $t('modals.peer-view.identifier') }}: {{ selectedPeer.PublicKey }}</li>
|
||||
<li>{{ $t('modals.peer-view.ip') }}: <span v-for="ip in selectedPeer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span></li>
|
||||
<li>{{ $t('modals.peer-view.user') }}: {{ selectedPeer.UserIdentifier }}</li>
|
||||
<li v-if="selectedPeer.Notes">{{ $t('modals.peer-view.notes') }}: {{ selectedPeer.Notes }}</li>
|
||||
<li v-if="selectedPeer.ExpiresAt">{{ $t('modals.peer-view.expiry-status') }}: {{ selectedPeer.ExpiresAt }}</li>
|
||||
<li v-if="selectedPeer.Disabled">{{ $t('modals.peer-view.disabled-status') }}: {{ selectedPeer.DisabledReason }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<img class="config-qr-img" :src="peers.ConfigQrUrl(props.peerId)" loading="lazy" alt="Configuration QR Code">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingStatus">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseStatus" aria-expanded="false" aria-controls="collapseStatus">
|
||||
{{ $t('modals.peer-view.section-status') }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseStatus" class="accordion-collapse collapse" aria-labelledby="headingStatus" data-bs-parent="#peerInformation" style="">
|
||||
<div class="accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>{{ $t('modals.peer-view.traffic') }}</h4>
|
||||
<p><i class="fas fa-long-arrow-alt-down" :title="$t('modals.peer-view.download')"></i> {{ selectedStats.BytesReceived }} Bytes / <i class="fas fa-long-arrow-alt-up" :title="$t('modals.peer-view.upload')"></i> {{ selectedStats.BytesTransmitted }} Bytes</p>
|
||||
<h4>{{ $t('modals.peer-view.connection-status') }}</h4>
|
||||
<ul>
|
||||
<li>{{ $t('modals.peer-view.pingable') }}: {{ selectedStats.IsPingable }}</li>
|
||||
<li>{{ $t('modals.peer-view.handshake') }}: {{ selectedStats.LastHandshake }}</li>
|
||||
<li>{{ $t('modals.peer-view.connected-since') }}: {{ selectedStats.LastSessionStart }}</li>
|
||||
<li>{{ $t('modals.peer-view.endpoint') }}: {{ selectedStats.EndpointAddress }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedInterface.Mode==='server'" class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingConfig">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseConfig" aria-expanded="false" aria-controls="collapseConfig">
|
||||
{{ $t('modals.peer-view.section-config') }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseConfig" class="accordion-collapse collapse" aria-labelledby="headingConfig" data-bs-parent="#peerInformation" style="">
|
||||
<div class="accordion-body">
|
||||
<Prism language="ini" :code="configString"></Prism>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button @click.prevent="download" type="button" class="btn btn-primary me-1">{{ $t('modals.peer-view.button-download') }}</button>
|
||||
<button @click.prevent="email" type="button" class="btn btn-primary me-1">{{ $t('modals.peer-view.button-email') }}</button>
|
||||
</div>
|
||||
<button @click.prevent="close" type="button" class="btn btn-secondary">{{ $t('general.close') }}</button>
|
||||
|
||||
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.config-qr-img {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
174
frontend/src/components/UserEditModal.vue
Normal file
174
frontend/src/components/UserEditModal.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {userStore} from "@/stores/users";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import {freshUser} from "@/helpers/models";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const users = userStore()
|
||||
|
||||
const props = defineProps({
|
||||
userId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedUser = computed(() => {
|
||||
return users.Find(props.userId)
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
if (selectedUser.value) {
|
||||
return t("modals.user-edit.headline-edit") + " " + selectedUser.value.Identifier
|
||||
}
|
||||
return t("modals.user-edit.headline-new")
|
||||
})
|
||||
|
||||
const formData = ref(freshUser())
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
if (!selectedUser.value) {
|
||||
formData.value = freshUser()
|
||||
} else { // fill existing userdata
|
||||
formData.value.Identifier = selectedUser.value.Identifier
|
||||
formData.value.Email = selectedUser.value.Email
|
||||
formData.value.Source = selectedUser.value.Source
|
||||
formData.value.IsAdmin = selectedUser.value.IsAdmin
|
||||
formData.value.Firstname = selectedUser.value.Firstname
|
||||
formData.value.Lastname = selectedUser.value.Lastname
|
||||
formData.value.Phone = selectedUser.value.Phone
|
||||
formData.value.Department = selectedUser.value.Department
|
||||
formData.value.Notes = selectedUser.value.Notes
|
||||
formData.value.Password = ""
|
||||
formData.value.Disabled = selectedUser.value.Disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
formData.value = freshUser()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
if (props.userId!=='#NEW#') {
|
||||
await users.UpdateUser(selectedUser.value.Identifier, formData.value)
|
||||
} else {
|
||||
await users.CreateUser(formData.value)
|
||||
}
|
||||
close()
|
||||
} catch (e) {
|
||||
notify({
|
||||
title: "Failed to save user!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
try {
|
||||
await users.DeleteUser(selectedUser.value.Identifier)
|
||||
close()
|
||||
} catch (e) {
|
||||
notify({
|
||||
title: "Failed to delete user!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<fieldset v-if="formData.Source==='db'">
|
||||
<legend class="mt-4">{{ $t('modals.user-edit.header-general') }}</legend>
|
||||
<div v-if="props.userId==='#NEW#'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.identifier.label') }}</label>
|
||||
<input v-model="formData.Identifier" class="form-control" :placeholder="$t('modals.user-edit.identifier.placeholder')" type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.source.label') }}</label>
|
||||
<input v-model="formData.Source" class="form-control" disabled="disabled" :placeholder="$t('modals.user-edit.source.placeholder')" type="text">
|
||||
</div>
|
||||
<div v-if="formData.Source==='db'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.password.label') }}</label>
|
||||
<input v-model="formData.Password" aria-describedby="passwordHelp" class="form-control" :placeholder="$t('modals.user-edit.password.placeholder')" type="text">
|
||||
<small v-if="props.userId!=='#NEW#'" id="passwordHelp" class="form-text text-muted">{{ $t('modals.user-edit.password.description') }}</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset v-if="formData.Source==='db'">
|
||||
<legend class="mt-4">{{ $t('modals.user-edit.header-personal') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.email.label') }}</label>
|
||||
<input v-model="formData.Email" class="form-control" :placeholder="$t('modals.user-edit.email.placeholder')" type="email">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.firstname.label') }}</label>
|
||||
<input v-model="formData.Firstname" class="form-control" :placeholder="$t('modals.user-edit.firstname.placeholder')" type="text">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.lastname.label') }}</label>
|
||||
<input v-model="formData.Lastname" class="form-control" :placeholder="$t('modals.user-edit.lastname.placeholder')" type="text">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.phone.label') }}</label>
|
||||
<input v-model="formData.Phone" class="form-control" :placeholder="$t('modals.user-edit.phone.placeholder')" type="text">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.department.label') }}</label>
|
||||
<input v-model="formData.Department" class="form-control" :placeholder="$t('modals.user-edit.department.placeholder')" type="text">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.user-edit.header-notes') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.notes.label') }}</label>
|
||||
<textarea v-model="formData.Notes" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.user-edit.header-state') }}</legend>
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="formData.Disabled" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" >{{ $t('modals.user-edit.disabled.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="formData.Locked" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" >{{ $t('modals.user-edit.locked.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch" v-if="formData.Source==='db'">
|
||||
<input v-model="formData.IsAdmin" checked="" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.user-edit.admin.label') }}</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.userId!=='#NEW#'&&formData.Source==='db'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
</div>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
143
frontend/src/components/UserViewModal.vue
Normal file
143
frontend/src/components/UserViewModal.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {userStore} from "../stores/users";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const users = userStore()
|
||||
|
||||
const props = defineProps({
|
||||
userId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedUser = computed(() => {
|
||||
let user = users.Find(props.userId)
|
||||
if (user) {
|
||||
return user
|
||||
}
|
||||
|
||||
return {} // return empty object to avoid "undefined" access problems
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
return t("modals.user-view.headline") + " " + selectedUser.value.Identifier
|
||||
})
|
||||
|
||||
const userPeers = computed(() => {
|
||||
return users.Peers
|
||||
})
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
await users.LoadUserPeers(selectedUser.value.Identifier)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#user">{{ $t('modals.user-view.tab-user') }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#peers">{{ $t('modals.user-view.tab-peers') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div id="interfaceTabs" class="tab-content">
|
||||
<div id="user" class="tab-pane fade active show">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<h4>{{ $t('modals.user-view.headline-info') }}</h4>
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.email') }}:</td>
|
||||
<td>{{selectedUser.Email}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.firstname') }}:</td>
|
||||
<td>{{selectedUser.Firstname}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.lastname') }}:</td>
|
||||
<td>{{selectedUser.Lastname}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.phone') }}:</td>
|
||||
<td>{{selectedUser.Phone}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.department') }}:</td>
|
||||
<td>{{selectedUser.Department}}</td>
|
||||
</tr>
|
||||
<tr v-if="selectedUser.Disabled">
|
||||
<td>{{ $t('modals.user-view.disabled') }}:</td>
|
||||
<td>{{selectedUser.DisabledReason}}</td>
|
||||
</tr>
|
||||
<tr v-if="selectedUser.Locked">
|
||||
<td>{{ $t('modals.user-view.locked') }}:</td>
|
||||
<td>{{selectedUser.LockedReason}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</li>
|
||||
<li class="list-group-item" v-if="selectedUser.Notes">
|
||||
<h4>{{ $t('modals.user-view.headline-notes') }}</h4>
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr><td>{{selectedUser.Notes}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="peers" class="tab-pane fade">
|
||||
<ul v-if="userPeers.length===0" class="list-group list-group-flush">
|
||||
<li class="list-group-item">{{ $t('modals.user-view.no-peers') }}</li>
|
||||
</ul>
|
||||
|
||||
<table v-if="userPeers.length!==0" id="peerTable" class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('modals.user-view.peers.name') }}</th>
|
||||
<th scope="col">{{ $t('modals.user-view.peers.interface') }}</th>
|
||||
<th scope="col">{{ $t('modals.user-view.peers.ip') }}</th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="peer in userPeers" :key="peer.Identifier">
|
||||
<td>{{peer.DisplayName}}</td>
|
||||
<td>{{peer.InterfaceIdentifier}}</td>
|
||||
<td>
|
||||
<span v-for="ip in peer.Addresses" :key="ip" class="badge pill bg-light">{{ ip }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="btn btn-primary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
7
frontend/src/components/icons/IconCommunity.vue
Normal file
7
frontend/src/components/icons/IconCommunity.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
frontend/src/components/icons/IconDocumentation.vue
Normal file
7
frontend/src/components/icons/IconDocumentation.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
frontend/src/components/icons/IconEcosystem.vue
Normal file
7
frontend/src/components/icons/IconEcosystem.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
frontend/src/components/icons/IconSupport.vue
Normal file
7
frontend/src/components/icons/IconSupport.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
19
frontend/src/components/icons/IconTooling.vue
Normal file
19
frontend/src/components/icons/IconTooling.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
7
frontend/src/helpers/encoding.js
Normal file
7
frontend/src/helpers/encoding.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export function base64_url_encode(input) {
|
||||
let output = btoa(input)
|
||||
output = output.replace('+', '.')
|
||||
output = output.replace('/', '_')
|
||||
output = output.replace('=', '-')
|
||||
return output
|
||||
}
|
95
frontend/src/helpers/fetch-wrapper.js
Normal file
95
frontend/src/helpers/fetch-wrapper.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import { authStore } from '@/stores/auth';
|
||||
import { securityStore } from '@/stores/security';
|
||||
|
||||
export const fetchWrapper = {
|
||||
url: apiUrl(),
|
||||
get: request('GET'),
|
||||
post: request('POST'),
|
||||
put: request('PUT'),
|
||||
delete: request('DELETE')
|
||||
};
|
||||
|
||||
export const apiWrapper = {
|
||||
url: apiUrl(),
|
||||
get: apiRequest('GET'),
|
||||
post: apiRequest('POST'),
|
||||
put: apiRequest('PUT'),
|
||||
delete: apiRequest('DELETE')
|
||||
};
|
||||
|
||||
// request can be used to query arbitrary URLs
|
||||
function request(method) {
|
||||
return (url, body = undefined) => {
|
||||
const requestOptions = {
|
||||
method,
|
||||
headers: getHeaders(url)
|
||||
};
|
||||
if (body) {
|
||||
requestOptions.headers['Content-Type'] = 'application/json';
|
||||
requestOptions.body = JSON.stringify(body);
|
||||
}
|
||||
return fetch(url, requestOptions).then(handleResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// apiRequest uses WGPORTAL_BACKEND_BASE_URL as base URL
|
||||
function apiRequest(method) {
|
||||
return (path, body = undefined) => {
|
||||
const url = WGPORTAL_BACKEND_BASE_URL + path
|
||||
const requestOptions = {
|
||||
method,
|
||||
headers: getHeaders(method, url)
|
||||
};
|
||||
if (body) {
|
||||
requestOptions.headers['Content-Type'] = 'application/json';
|
||||
requestOptions.body = JSON.stringify(body);
|
||||
}
|
||||
return fetch(url, requestOptions).then(handleResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// apiUrl uses WGPORTAL_BACKEND_BASE_URL as base URL
|
||||
function apiUrl() {
|
||||
return (path) => {
|
||||
return WGPORTAL_BACKEND_BASE_URL + path
|
||||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
|
||||
function getHeaders(method, url) {
|
||||
// return auth header with jwt if user is logged in and request is to the api url
|
||||
const auth = authStore();
|
||||
const sec = securityStore();
|
||||
const isApiUrl = url.startsWith(WGPORTAL_BACKEND_BASE_URL);
|
||||
|
||||
let headers = {};
|
||||
if (isApiUrl && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
||||
headers["X-CSRF-TOKEN"] = sec.CsrfToken;
|
||||
}
|
||||
if (isApiUrl && auth.IsAuthenticated) {
|
||||
headers["X-FRONTEND-UID"] = auth.UserIdentifier;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function handleResponse(response) {
|
||||
return response.text().then(text => {
|
||||
const data = text && JSON.parse(text);
|
||||
|
||||
if (!response.ok) {
|
||||
const auth = authStore();
|
||||
if ([401, 403].includes(response.status) && auth.IsAuthenticated) {
|
||||
console.log("automatic logout initiated...");
|
||||
// auto logout if 401 Unauthorized or 403 Forbidden response returned from api
|
||||
auth.Logout();
|
||||
}
|
||||
|
||||
const error = (data && data.Message) || response.statusText;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
164
frontend/src/helpers/models.js
Normal file
164
frontend/src/helpers/models.js
Normal file
@@ -0,0 +1,164 @@
|
||||
|
||||
export function freshInterface() {
|
||||
return {
|
||||
Disabled: false,
|
||||
DisplayName: "",
|
||||
Identifier: "",
|
||||
Mode: "server",
|
||||
|
||||
PublicKey: "",
|
||||
PrivateKey: "",
|
||||
|
||||
ListenPort: 51820,
|
||||
Addresses: [],
|
||||
DnsStr: [],
|
||||
DnsSearch: [],
|
||||
|
||||
Mtu: 0,
|
||||
FirewallMark: 0,
|
||||
RoutingTable: "",
|
||||
|
||||
PreUp: "",
|
||||
PostUp: "",
|
||||
PreDown: "",
|
||||
PostDown: "",
|
||||
|
||||
SaveConfig: false,
|
||||
|
||||
// Peer defaults
|
||||
|
||||
PeerDefNetwork: [],
|
||||
PeerDefDns: [],
|
||||
PeerDefDnsSearch: [],
|
||||
PeerDefEndpoint: "",
|
||||
PeerDefAllowedIPs: [],
|
||||
PeerDefMtu: 0,
|
||||
PeerDefPersistentKeepalive: 0,
|
||||
PeerDefFirewallMark: 0,
|
||||
PeerDefRoutingTable: "",
|
||||
PeerDefPreUp: "",
|
||||
PeerDefPostUp: "",
|
||||
PeerDefPreDown: "",
|
||||
PeerDefPostDown: "",
|
||||
|
||||
TotalPeers: 0,
|
||||
EnabledPeers: 0
|
||||
}
|
||||
}
|
||||
|
||||
export function freshPeer() {
|
||||
return {
|
||||
Identifier: "",
|
||||
DisplayName: "",
|
||||
UserIdentifier: "",
|
||||
InterfaceIdentifier: "",
|
||||
Disabled: false,
|
||||
ExpiresAt: null,
|
||||
Notes: "",
|
||||
|
||||
Endpoint: {
|
||||
Value: "",
|
||||
Overridable: true,
|
||||
},
|
||||
EndpointPublicKey: {
|
||||
Value: "",
|
||||
Overridable: true,
|
||||
},
|
||||
AllowedIPs: {
|
||||
Value: [],
|
||||
Overridable: true,
|
||||
},
|
||||
ExtraAllowedIPs: [],
|
||||
PresharedKey: "",
|
||||
PersistentKeepalive: {
|
||||
Value: 0,
|
||||
Overridable: true,
|
||||
},
|
||||
|
||||
PrivateKey: "",
|
||||
PublicKey: "",
|
||||
|
||||
Mode: "client",
|
||||
|
||||
Addresses: [],
|
||||
CheckAliveAddress: "",
|
||||
Dns: {
|
||||
Value: [],
|
||||
Overridable: true,
|
||||
},
|
||||
DnsSearch: {
|
||||
Value: [],
|
||||
Overridable: true,
|
||||
},
|
||||
Mtu: {
|
||||
Value: 0,
|
||||
Overridable: true,
|
||||
},
|
||||
FirewallMark: {
|
||||
Value: 0,
|
||||
Overridable: true,
|
||||
},
|
||||
RoutingTable: {
|
||||
Value: "",
|
||||
Overridable: true,
|
||||
},
|
||||
|
||||
PreUp: {
|
||||
Value: "",
|
||||
Overridable: true,
|
||||
},
|
||||
PostUp: {
|
||||
Value: "",
|
||||
Overridable: true,
|
||||
},
|
||||
PreDown: {
|
||||
Value: "",
|
||||
Overridable: true,
|
||||
},
|
||||
PostDown: {
|
||||
Value: "",
|
||||
Overridable: true,
|
||||
},
|
||||
|
||||
// Internal value
|
||||
IgnoreGlobalSettings: false
|
||||
}
|
||||
}
|
||||
|
||||
export function freshUser() {
|
||||
return {
|
||||
Identifier: "",
|
||||
|
||||
Email: "",
|
||||
Source: "db",
|
||||
IsAdmin: false,
|
||||
|
||||
Firstname: "",
|
||||
Lastname: "",
|
||||
Phone: "",
|
||||
Department: "",
|
||||
Notes: "",
|
||||
|
||||
Password: "",
|
||||
|
||||
Disabled: false,
|
||||
DisabledReason: "",
|
||||
Locked: false,
|
||||
LockedReason: "",
|
||||
|
||||
PeerCount: 0
|
||||
}
|
||||
}
|
||||
|
||||
export function freshStats() {
|
||||
return {
|
||||
IsConnected: false,
|
||||
IsPingable: false,
|
||||
LastHandshake: null,
|
||||
LastPing: null,
|
||||
LastSessionStart: null,
|
||||
BytesTransmitted: 0,
|
||||
BytesReceived: 0,
|
||||
EndpointAddress: ""
|
||||
}
|
||||
}
|
14
frontend/src/helpers/validators.js
Normal file
14
frontend/src/helpers/validators.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import isCidr from "is-cidr";
|
||||
import {isIP} from 'is-ip';
|
||||
|
||||
export function validateCIDR(value) {
|
||||
return isCidr(value) !== 0
|
||||
}
|
||||
|
||||
export function validateIP(value) {
|
||||
return isIP(value)
|
||||
}
|
||||
|
||||
export function validateDomain(value) {
|
||||
return true
|
||||
}
|
27
frontend/src/lang/index.js
Normal file
27
frontend/src/lang/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// src/lang/index.js
|
||||
import de from './translations/de.json';
|
||||
import en from './translations/en.json';
|
||||
import {createI18n} from "vue-i18n";
|
||||
|
||||
function getStoredLanguage() {
|
||||
let initialLang = localStorage.getItem('wgLang');
|
||||
if (!initialLang) {
|
||||
initialLang = "en"
|
||||
}
|
||||
return initialLang
|
||||
}
|
||||
|
||||
// Create i18n instance with options
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
allowComposition: true,
|
||||
locale: getStoredLanguage(), // set locale
|
||||
fallbackLocale: "en", // set fallback locale
|
||||
messages: {
|
||||
"de": de,
|
||||
"en": en
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n
|
489
frontend/src/lang/translations/de.json
Normal file
489
frontend/src/lang/translations/de.json
Normal file
@@ -0,0 +1,489 @@
|
||||
{
|
||||
"general": {
|
||||
"pagination": {
|
||||
"size": "Anzahl an Elementen",
|
||||
"all": "Alle (langsam)"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Suche...",
|
||||
"button": "Suchen"
|
||||
},
|
||||
"select-all": "Alle auswählen",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"cancel": "Abbrechen",
|
||||
"close": "Schließen",
|
||||
"save": "Speichern",
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"login": {
|
||||
"headline": "Bitte melden Sie sich an",
|
||||
"username": {
|
||||
"label": "Benutzername",
|
||||
"placeholder": "Bitte geben Sie Ihren Benutzernamen ein"
|
||||
},
|
||||
"password": {
|
||||
"label": "Kennwort",
|
||||
"placeholder": "Bitte geben Sie Ihr Passwort ein"
|
||||
},
|
||||
"button": "Anmelden"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
"interfaces": "Schnittstellen",
|
||||
"users": "Benutzer",
|
||||
"lang": "Sprache ändern",
|
||||
"profile": "Mein Profil",
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden"
|
||||
},
|
||||
"home": {
|
||||
"headline": "WireGuard® VPN Portal",
|
||||
"info-headline": "Mehr Informationen",
|
||||
"abstract": "WireGuard® ist ein extrem einfaches, aber dennoch schnelles und modernes VPN, das modernste Kryptographie nutzt. Es zielt darauf ab, schneller, einfacher, schlanker und nützlicher als IPsec zu sein, während es die massiven Kopfschmerzen vermeidet. Es soll wesentlich leistungsfähiger sein als OpenVPN.",
|
||||
"installation": {
|
||||
"box-header": "WireGuard Installation",
|
||||
"headline": "Installation",
|
||||
"content": "Die Installationsanweisungen für die Client-Software finden Sie auf der offiziellen WireGuard-Website.",
|
||||
"btn": "Anleitung öffnen"
|
||||
},
|
||||
"about-wg": {
|
||||
"box-header": "Über WireGuard",
|
||||
"headline": "Über",
|
||||
"content": "WireGuard® ist ein extrem einfaches, aber schnelles und modernes VPN, das modernste Kryptographie verwendet.",
|
||||
"button": "Details"
|
||||
},
|
||||
"about-portal": {
|
||||
"box-header": "Über WireGuard Portal",
|
||||
"headline": "WireGuard Portal",
|
||||
"content": "WireGuard Portal ist ein einfaches, webbasiertes Konfigurationsportal für WireGuard.",
|
||||
"button": "Details"
|
||||
},
|
||||
"profiles": {
|
||||
"headline": "VPN Profile",
|
||||
"abstract": "Über Ihr Benutzerprofil können Sie auf Ihre persönlichen VPN-Konfigurationen zugreifen und diese herunterladen.",
|
||||
"content": "Um alle Ihre konfigurierten Profile zu finden, klicken Sie auf die Schaltfläche unten.",
|
||||
"button": "Mein Profil öffnen"
|
||||
},
|
||||
"admin": {
|
||||
"headline": "Verwaltungsbereich",
|
||||
"abstract": "Im Administrationsbereich können Sie VPN-Zugänge und die Serverschnittstelle sowie die Benutzer, die sich am VPN-Portal anmelden dürfen, verwalten.",
|
||||
"content": "",
|
||||
"button-admin": "Schnittstellenverwaltung",
|
||||
"button-user": "Benutzerverwaltung"
|
||||
}
|
||||
},
|
||||
"interfaces": {
|
||||
"headline": "Schnittstellenverwaltung",
|
||||
"headline-peers": "Current VPN Peers",
|
||||
"headline-endpoints": "Current Endpoints",
|
||||
"no-interface": {
|
||||
"default-selection": "No Interface available",
|
||||
"headline": "No interfaces found...",
|
||||
"abstract": "Click the plus button above to create a new WireGuard interface."
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "No peers available",
|
||||
"abstract": "Currently, there are no peers available for the selected WireGuard interface."
|
||||
},
|
||||
"table-heading": {
|
||||
"name": "Name",
|
||||
"user": "User",
|
||||
"ip": "IP's",
|
||||
"endpoint": "Endpoint",
|
||||
"status": "Status"
|
||||
},
|
||||
"interface": {
|
||||
"headline": "Interface status for",
|
||||
"mode": "mode",
|
||||
"key": "Public Key",
|
||||
"endpoint": "Public Endpoint",
|
||||
"port": "Listening Port",
|
||||
"peers": "Enabled Peers",
|
||||
"total-peers": "Total Peers",
|
||||
"endpoints": "Enabled Endpoints",
|
||||
"total-endpoints": "Total Endpoints",
|
||||
"ip": "IP Address",
|
||||
"default-allowed-ip": "Default allowed IPs",
|
||||
"dns": "DNS Servers",
|
||||
"mtu": "MTU",
|
||||
"default-keep-alive": "Default Keepalive Interval",
|
||||
"button-show-config": "Show configuration",
|
||||
"button-download-config": "Download configuration",
|
||||
"button-store-config": "Store configuration for wg-quick",
|
||||
"button-edit": "Edit interface"
|
||||
},
|
||||
"button-add-interface": "Add Interface",
|
||||
"button-add-peer": "Add Peer",
|
||||
"button-add-peers": "Add Multiple Peers",
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer",
|
||||
"peer-disabled": "Peer is disabled, reason:",
|
||||
"peer-expiring": "Peer is expiring at",
|
||||
"peer-connected": "Connected",
|
||||
"peer-not-connected": "Not Connected",
|
||||
"peer-handshake": "Last handshake:"
|
||||
},
|
||||
"users": {
|
||||
"headline": "Benutzerverwaltung",
|
||||
"table-heading": {
|
||||
"id": "ID",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"source": "Source",
|
||||
"peers": "Peers",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"no-user": {
|
||||
"headline": "No users available",
|
||||
"abstract": "Currently, there are no users registered with WireGuard Portal."
|
||||
},
|
||||
"button-add-user": "Add User",
|
||||
"button-show-user": "Show User",
|
||||
"button-edit-user": "Edit User",
|
||||
"user-disabled": "User is disabled, reason:",
|
||||
"user-locked": "Account is locked, reason:",
|
||||
"admin": "User has administrator privileges",
|
||||
"no-admin": "User has no administrator privileges"
|
||||
},
|
||||
"profile": {
|
||||
"headline": "Meine VPN-Konfigurationen",
|
||||
"table-heading": {
|
||||
"name": "Name",
|
||||
"ip": "IP's",
|
||||
"stats": "Status",
|
||||
"interface": "Server Interface"
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "No peers available",
|
||||
"abstract": "Currently, there are no peers associated with your user profile."
|
||||
},
|
||||
"peer-connected": "Connected",
|
||||
"button-add-peer": "Add Peer",
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer"
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "User Account:",
|
||||
"tab-user": "Information",
|
||||
"tab-peers": "Peers",
|
||||
"headline-info": "User Information:",
|
||||
"headline-notes": "Notes:",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"phone": "Phone number",
|
||||
"department": "Department",
|
||||
"disabled": "Account Disabled",
|
||||
"locked": "Account Locked",
|
||||
"no-peers": "User has no associated peers.",
|
||||
"peers": {
|
||||
"name": "Name",
|
||||
"interface": "Interface",
|
||||
"ip": "IP's"
|
||||
}
|
||||
},
|
||||
"user-edit": {
|
||||
"headline-edit": "Edit user:",
|
||||
"headline-new": "New user",
|
||||
"header-general": "General",
|
||||
"header-personal": "User Information",
|
||||
"header-notes": "Notes",
|
||||
"header-state": "State",
|
||||
"identifier": {
|
||||
"label": "Identifier",
|
||||
"placeholder": "The unique user identifier"
|
||||
},
|
||||
"source": {
|
||||
"label": "Source",
|
||||
"placeholder": "The user source"
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"placeholder": "A super secret password",
|
||||
"description": "Leave this field blank to keep current password."
|
||||
},
|
||||
"email": {
|
||||
"label": "Email",
|
||||
"placeholder": "The email address"
|
||||
},
|
||||
"phone": {
|
||||
"label": "Phone",
|
||||
"placeholder": "The phone number"
|
||||
},
|
||||
"department": {
|
||||
"label": "Department",
|
||||
"placeholder": "The department"
|
||||
},
|
||||
"firstname": {
|
||||
"label": "Firstname",
|
||||
"placeholder": "Firstname"
|
||||
},
|
||||
"lastname": {
|
||||
"label": "Lastname",
|
||||
"placeholder": "Lastname"
|
||||
},
|
||||
"notes": {
|
||||
"label": "Notes",
|
||||
"placeholder": ""
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Disabled (no WireGuard connection and no login possible)"
|
||||
},
|
||||
"locked": {
|
||||
"label": "Locked (no login possible, WireGuard connections still work)"
|
||||
},
|
||||
"admin": {
|
||||
"label": "Is Admin"
|
||||
}
|
||||
},
|
||||
"interface-view": {
|
||||
"headline": "Config for Interface:"
|
||||
},
|
||||
"interface-edit": {
|
||||
"headline-edit": "Edit Interface:",
|
||||
"headline-new": "New Interface",
|
||||
"tab-interface": "Interface",
|
||||
"tab-peerdef": "Peer Defaults",
|
||||
"header-general": "General",
|
||||
"header-network": "Network",
|
||||
"header-crypto": "Cryptography",
|
||||
"header-hooks": "Interface Hooks",
|
||||
"header-peer-hooks": "Hooks",
|
||||
"header-state": "State",
|
||||
"identifier": {
|
||||
"label": "Identifier",
|
||||
"placeholder": "The unique interface identifier"
|
||||
},
|
||||
"mode": {
|
||||
"label": "Interface Mode",
|
||||
"server": "Server Mode",
|
||||
"client": "Client Mode",
|
||||
"any": "Unknown Mode"
|
||||
},
|
||||
"display-name": {
|
||||
"label": "Display Name",
|
||||
"placeholder": "The descriptive name for the interface"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
"placeholder": "The public key"
|
||||
},
|
||||
"ip": {
|
||||
"label": "IP Addresses",
|
||||
"placeholder": "IP Addresses (CIDR format)"
|
||||
},
|
||||
"listen-port": {
|
||||
"label": "Listen Port",
|
||||
"placeholder": "The listening port"
|
||||
},
|
||||
"dns": {
|
||||
"label": "DNS Server",
|
||||
"placeholder": "The DNS servers that should be used"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "DNS Search Domains",
|
||||
"placeholder": "DNS search prefixes"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The interface MTU (0 = keep default)"
|
||||
},
|
||||
"firewall-mark": {
|
||||
"label": "Firewall Mark",
|
||||
"placeholder": "Firewall mark that is applied to outgoing traffic. (0 = automatic)"
|
||||
},
|
||||
"routing-table": {
|
||||
"label": "Routing Table",
|
||||
"placeholder": "The routing table ID",
|
||||
"description": "Special cases: off = do not manage routes, 0 = automatic"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pre-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pre-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Interface Disabled"
|
||||
},
|
||||
"save-config": {
|
||||
"label": "Automatically save wg-quick config"
|
||||
},
|
||||
"defaults": {
|
||||
"endpoint": {
|
||||
"label": "Endpoint Address",
|
||||
"placeholder": "Endpoint Address",
|
||||
"description": "The endpoint address that peers will connect to."
|
||||
},
|
||||
"networks": {
|
||||
"label": "IP Networks",
|
||||
"placeholder": "Network Addresses",
|
||||
"description": "Peers will get IP addresses from those subnets."
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Allowed IP Addresses",
|
||||
"placeholder": "Default Allowed IP Addresses"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The client MTU (0 = keep default)"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Keep Alive Interval",
|
||||
"placeholder": "Persistent Keepalive (0 = default)"
|
||||
}
|
||||
},
|
||||
|
||||
"button-apply-defaults": "Apply Peer Defaults"
|
||||
},
|
||||
"peer-view": {
|
||||
"headline-peer": "Peer:",
|
||||
"headline-endpoint": "Endpoint:",
|
||||
"section-info": "Peer Information",
|
||||
"section-status": "Current Status",
|
||||
"section-config": "Configuration",
|
||||
"identifier": "Identifier",
|
||||
"ip": "IP Addresses",
|
||||
"user": "Associated User",
|
||||
"notes": "Notes",
|
||||
"expiry-status": "Expires At",
|
||||
"disabled-status": "Disabled At",
|
||||
"traffic": "Traffic",
|
||||
"connection-status": "Connection Stats",
|
||||
"upload": "Uploaded Bytes (from Server to Peer)",
|
||||
"download": "Downloaded Bytes (from Peer to Server)",
|
||||
"pingable": "Is Pingable",
|
||||
"handshake": "Last Handshake",
|
||||
"connected-since": "Connected since",
|
||||
"endpoint": "Endpoint",
|
||||
"button-download": "Download configuration",
|
||||
"button-email": "Send configuration via E-Mail"
|
||||
},
|
||||
"peer-edit": {
|
||||
"headline-edit-peer": "Edit peer:",
|
||||
"headline-edit-endpoint": "Edit endpoint:",
|
||||
"headline-new-peer": "Create peer",
|
||||
"headline-new-endpoint": "Create endpoint",
|
||||
"header-general": "General",
|
||||
"header-network": "Network",
|
||||
"header-crypto": "Cryptography",
|
||||
"header-hooks": "Hooks (Executed on Peer)",
|
||||
"header-state": "State",
|
||||
"display-name": {
|
||||
"label": "Display Name",
|
||||
"placeholder": "The descriptive name for the peer"
|
||||
},
|
||||
"linked-user": {
|
||||
"label": "Linked User",
|
||||
"placeholder": "The user account which owns this peer"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
"placeholder": "The public key"
|
||||
},
|
||||
"preshared-key": {
|
||||
"label": "Preshared Key",
|
||||
"placeholder": "Optional pre-shared key"
|
||||
},
|
||||
"endpoint-public-key": {
|
||||
"label": "Endpoint public Key",
|
||||
"placeholder": "The public key of the remote endpoint"
|
||||
},
|
||||
"endpoint": {
|
||||
"label": "Endpoint Address",
|
||||
"placeholder": "The address of the remote endpoint"
|
||||
},
|
||||
"ip": {
|
||||
"label": "IP Addresses",
|
||||
"placeholder": "IP Addresses (CIDR format)"
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Allowed IP Addresses",
|
||||
"placeholder": "Allowed IP Addresses (CIDR format)"
|
||||
},
|
||||
"extra-allowed-ip": {
|
||||
"label": "Extra allowed IP Addresses",
|
||||
"placeholder": "Extra allowed IP's (Server Sided)",
|
||||
"description": "Those IP's will be added on the remote WireGuard interface as allowed IP's."
|
||||
},
|
||||
"dns": {
|
||||
"label": "DNS Server",
|
||||
"placeholder": "The DNS servers that should be used"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "DNS Search Domains",
|
||||
"placeholder": "DNS search prefixes"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Keep Alive Interval",
|
||||
"placeholder": "Persistent Keepalive (0 = default)"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The client MTU (0 = keep default)"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pre-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pre-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Peer Disabled"
|
||||
},
|
||||
"ignore-global": {
|
||||
"label": "Ignore global settings"
|
||||
},
|
||||
"expires-at": {
|
||||
"label": "Expiry date"
|
||||
}
|
||||
},
|
||||
"peer-multi-create": {
|
||||
"headline-peer": "Create multiple peers",
|
||||
"headline-endpoint": "Create multiple endpoints",
|
||||
"identifiers": {
|
||||
"label": "User Identifiers",
|
||||
"placeholder": "User Identifiers",
|
||||
"description": "A user identifier (the username) for which a peer should be created."
|
||||
},
|
||||
"prefix": {
|
||||
"headline-peer": "Peer:",
|
||||
"headline-endpoint": "Endpoint:",
|
||||
"label": "Display Name Prefix",
|
||||
"placeholder": "The prefix",
|
||||
"description": "A prefix that is added to the peers display name."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
489
frontend/src/lang/translations/en.json
Normal file
489
frontend/src/lang/translations/en.json
Normal file
@@ -0,0 +1,489 @@
|
||||
{
|
||||
"general": {
|
||||
"pagination": {
|
||||
"size": "Number of Elements",
|
||||
"all": "All (slow)"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search...",
|
||||
"button": "Search"
|
||||
},
|
||||
"select-all": "Select all",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"save": "Save",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"login": {
|
||||
"headline": "Please sign in",
|
||||
"username": {
|
||||
"label": "Username",
|
||||
"placeholder": "Please enter your username"
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"placeholder": "Please enter your password"
|
||||
},
|
||||
"button": "Sign in"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
"interfaces": "Interfaces",
|
||||
"users": "Users",
|
||||
"lang": "Toggle Language",
|
||||
"profile": "My Profile",
|
||||
"login": "Login",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"home": {
|
||||
"headline": "WireGuard® VPN Portal",
|
||||
"info-headline": "More Information",
|
||||
"abstract": "WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN.",
|
||||
"installation": {
|
||||
"box-header": "WireGuard Installation",
|
||||
"headline": "Installation",
|
||||
"content": "Installation instructions for client software can be found on the official WireGuard website.",
|
||||
"btn": "Open Instructions"
|
||||
},
|
||||
"about-wg": {
|
||||
"box-header": "About WireGuard",
|
||||
"headline": "About",
|
||||
"content": "WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography.",
|
||||
"button": "More"
|
||||
},
|
||||
"about-portal": {
|
||||
"box-header": "About WireGuard Portal",
|
||||
"headline": "WireGuard Portal",
|
||||
"content": "WireGuard Portal is a simple, web based configuration portal for WireGuard.",
|
||||
"button": "More"
|
||||
},
|
||||
"profiles": {
|
||||
"headline": "VPN Profiles",
|
||||
"abstract": "You can access and download your personal VPN configurations via your Userprofile.",
|
||||
"content": "To find all your configured profiles click on the button below.",
|
||||
"button": "Open my profile"
|
||||
},
|
||||
"admin": {
|
||||
"headline": "Administration Area",
|
||||
"abstract": "In the administration area you can manage WireGuard peers and the server interface as well as users that are allowed to log in to the WireGuard Portal.",
|
||||
"content": "",
|
||||
"button-admin": "Open Server Administration",
|
||||
"button-user": "Open User Administration"
|
||||
}
|
||||
},
|
||||
"interfaces": {
|
||||
"headline": "Interface Administration",
|
||||
"headline-peers": "Current VPN Peers",
|
||||
"headline-endpoints": "Current Endpoints",
|
||||
"no-interface": {
|
||||
"default-selection": "No Interface available",
|
||||
"headline": "No interfaces found...",
|
||||
"abstract": "Click the plus button above to create a new WireGuard interface."
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "No peers available",
|
||||
"abstract": "Currently, there are no peers available for the selected WireGuard interface."
|
||||
},
|
||||
"table-heading": {
|
||||
"name": "Name",
|
||||
"user": "User",
|
||||
"ip": "IP's",
|
||||
"endpoint": "Endpoint",
|
||||
"status": "Status"
|
||||
},
|
||||
"interface": {
|
||||
"headline": "Interface status for",
|
||||
"mode": "mode",
|
||||
"key": "Public Key",
|
||||
"endpoint": "Public Endpoint",
|
||||
"port": "Listening Port",
|
||||
"peers": "Enabled Peers",
|
||||
"total-peers": "Total Peers",
|
||||
"endpoints": "Enabled Endpoints",
|
||||
"total-endpoints": "Total Endpoints",
|
||||
"ip": "IP Address",
|
||||
"default-allowed-ip": "Default allowed IPs",
|
||||
"dns": "DNS Servers",
|
||||
"mtu": "MTU",
|
||||
"default-keep-alive": "Default Keepalive Interval",
|
||||
"button-show-config": "Show configuration",
|
||||
"button-download-config": "Download configuration",
|
||||
"button-store-config": "Store configuration for wg-quick",
|
||||
"button-edit": "Edit interface"
|
||||
},
|
||||
"button-add-interface": "Add Interface",
|
||||
"button-add-peer": "Add Peer",
|
||||
"button-add-peers": "Add Multiple Peers",
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer",
|
||||
"peer-disabled": "Peer is disabled, reason:",
|
||||
"peer-expiring": "Peer is expiring at",
|
||||
"peer-connected": "Connected",
|
||||
"peer-not-connected": "Not Connected",
|
||||
"peer-handshake": "Last handshake:"
|
||||
},
|
||||
"users": {
|
||||
"headline": "User Administration",
|
||||
"table-heading": {
|
||||
"id": "ID",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"source": "Source",
|
||||
"peers": "Peers",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"no-user": {
|
||||
"headline": "No users available",
|
||||
"abstract": "Currently, there are no users registered with WireGuard Portal."
|
||||
},
|
||||
"button-add-user": "Add User",
|
||||
"button-show-user": "Show User",
|
||||
"button-edit-user": "Edit User",
|
||||
"user-disabled": "User is disabled, reason:",
|
||||
"user-locked": "Account is locked, reason:",
|
||||
"admin": "User has administrator privileges",
|
||||
"no-admin": "User has no administrator privileges"
|
||||
},
|
||||
"profile": {
|
||||
"headline": "My VPN Peers",
|
||||
"table-heading": {
|
||||
"name": "Name",
|
||||
"ip": "IP's",
|
||||
"stats": "Status",
|
||||
"interface": "Server Interface"
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "No peers available",
|
||||
"abstract": "Currently, there are no peers associated with your user profile."
|
||||
},
|
||||
"peer-connected": "Connected",
|
||||
"button-add-peer": "Add Peer",
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer"
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "User Account:",
|
||||
"tab-user": "Information",
|
||||
"tab-peers": "Peers",
|
||||
"headline-info": "User Information:",
|
||||
"headline-notes": "Notes:",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"phone": "Phone number",
|
||||
"department": "Department",
|
||||
"disabled": "Account Disabled",
|
||||
"locked": "Account Locked",
|
||||
"no-peers": "User has no associated peers.",
|
||||
"peers": {
|
||||
"name": "Name",
|
||||
"interface": "Interface",
|
||||
"ip": "IP's"
|
||||
}
|
||||
},
|
||||
"user-edit": {
|
||||
"headline-edit": "Edit user:",
|
||||
"headline-new": "New user",
|
||||
"header-general": "General",
|
||||
"header-personal": "User Information",
|
||||
"header-notes": "Notes",
|
||||
"header-state": "State",
|
||||
"identifier": {
|
||||
"label": "Identifier",
|
||||
"placeholder": "The unique user identifier"
|
||||
},
|
||||
"source": {
|
||||
"label": "Source",
|
||||
"placeholder": "The user source"
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"placeholder": "A super secret password",
|
||||
"description": "Leave this field blank to keep current password."
|
||||
},
|
||||
"email": {
|
||||
"label": "Email",
|
||||
"placeholder": "The email address"
|
||||
},
|
||||
"phone": {
|
||||
"label": "Phone",
|
||||
"placeholder": "The phone number"
|
||||
},
|
||||
"department": {
|
||||
"label": "Department",
|
||||
"placeholder": "The department"
|
||||
},
|
||||
"firstname": {
|
||||
"label": "Firstname",
|
||||
"placeholder": "Firstname"
|
||||
},
|
||||
"lastname": {
|
||||
"label": "Lastname",
|
||||
"placeholder": "Lastname"
|
||||
},
|
||||
"notes": {
|
||||
"label": "Notes",
|
||||
"placeholder": ""
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Disabled (no WireGuard connection and no login possible)"
|
||||
},
|
||||
"locked": {
|
||||
"label": "Locked (no login possible, WireGuard connections still work)"
|
||||
},
|
||||
"admin": {
|
||||
"label": "Is Admin"
|
||||
}
|
||||
},
|
||||
"interface-view": {
|
||||
"headline": "Config for Interface:"
|
||||
},
|
||||
"interface-edit": {
|
||||
"headline-edit": "Edit Interface:",
|
||||
"headline-new": "New Interface",
|
||||
"tab-interface": "Interface",
|
||||
"tab-peerdef": "Peer Defaults",
|
||||
"header-general": "General",
|
||||
"header-network": "Network",
|
||||
"header-crypto": "Cryptography",
|
||||
"header-hooks": "Interface Hooks",
|
||||
"header-peer-hooks": "Hooks",
|
||||
"header-state": "State",
|
||||
"identifier": {
|
||||
"label": "Identifier",
|
||||
"placeholder": "The unique interface identifier"
|
||||
},
|
||||
"mode": {
|
||||
"label": "Interface Mode",
|
||||
"server": "Server Mode",
|
||||
"client": "Client Mode",
|
||||
"any": "Unknown Mode"
|
||||
},
|
||||
"display-name": {
|
||||
"label": "Display Name",
|
||||
"placeholder": "The descriptive name for the interface"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
"placeholder": "The public key"
|
||||
},
|
||||
"ip": {
|
||||
"label": "IP Addresses",
|
||||
"placeholder": "IP Addresses (CIDR format)"
|
||||
},
|
||||
"listen-port": {
|
||||
"label": "Listen Port",
|
||||
"placeholder": "The listening port"
|
||||
},
|
||||
"dns": {
|
||||
"label": "DNS Server",
|
||||
"placeholder": "The DNS servers that should be used"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "DNS Search Domains",
|
||||
"placeholder": "DNS search prefixes"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The interface MTU (0 = keep default)"
|
||||
},
|
||||
"firewall-mark": {
|
||||
"label": "Firewall Mark",
|
||||
"placeholder": "Firewall mark that is applied to outgoing traffic. (0 = automatic)"
|
||||
},
|
||||
"routing-table": {
|
||||
"label": "Routing Table",
|
||||
"placeholder": "The routing table ID",
|
||||
"description": "Special cases: off = do not manage routes, 0 = automatic"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pre-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pre-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Interface Disabled"
|
||||
},
|
||||
"save-config": {
|
||||
"label": "Automatically save wg-quick config"
|
||||
},
|
||||
"defaults": {
|
||||
"endpoint": {
|
||||
"label": "Endpoint Address",
|
||||
"placeholder": "Endpoint Address",
|
||||
"description": "The endpoint address that peers will connect to."
|
||||
},
|
||||
"networks": {
|
||||
"label": "IP Networks",
|
||||
"placeholder": "Network Addresses",
|
||||
"description": "Peers will get IP addresses from those subnets."
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Allowed IP Addresses",
|
||||
"placeholder": "Default Allowed IP Addresses"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The client MTU (0 = keep default)"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Keep Alive Interval",
|
||||
"placeholder": "Persistent Keepalive (0 = default)"
|
||||
}
|
||||
},
|
||||
|
||||
"button-apply-defaults": "Apply Peer Defaults"
|
||||
},
|
||||
"peer-view": {
|
||||
"headline-peer": "Peer:",
|
||||
"headline-endpoint": "Endpoint:",
|
||||
"section-info": "Peer Information",
|
||||
"section-status": "Current Status",
|
||||
"section-config": "Configuration",
|
||||
"identifier": "Identifier",
|
||||
"ip": "IP Addresses",
|
||||
"user": "Associated User",
|
||||
"notes": "Notes",
|
||||
"expiry-status": "Expires At",
|
||||
"disabled-status": "Disabled At",
|
||||
"traffic": "Traffic",
|
||||
"connection-status": "Connection Stats",
|
||||
"upload": "Uploaded Bytes (from Server to Peer)",
|
||||
"download": "Downloaded Bytes (from Peer to Server)",
|
||||
"pingable": "Is Pingable",
|
||||
"handshake": "Last Handshake",
|
||||
"connected-since": "Connected since",
|
||||
"endpoint": "Endpoint",
|
||||
"button-download": "Download configuration",
|
||||
"button-email": "Send configuration via E-Mail"
|
||||
},
|
||||
"peer-edit": {
|
||||
"headline-edit-peer": "Edit peer:",
|
||||
"headline-edit-endpoint": "Edit endpoint:",
|
||||
"headline-new-peer": "Create peer",
|
||||
"headline-new-endpoint": "Create endpoint",
|
||||
"header-general": "General",
|
||||
"header-network": "Network",
|
||||
"header-crypto": "Cryptography",
|
||||
"header-hooks": "Hooks (Executed on Peer)",
|
||||
"header-state": "State",
|
||||
"display-name": {
|
||||
"label": "Display Name",
|
||||
"placeholder": "The descriptive name for the peer"
|
||||
},
|
||||
"linked-user": {
|
||||
"label": "Linked User",
|
||||
"placeholder": "The user account which owns this peer"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
"placeholder": "The public key"
|
||||
},
|
||||
"preshared-key": {
|
||||
"label": "Preshared Key",
|
||||
"placeholder": "Optional pre-shared key"
|
||||
},
|
||||
"endpoint-public-key": {
|
||||
"label": "Endpoint public Key",
|
||||
"placeholder": "The public key of the remote endpoint"
|
||||
},
|
||||
"endpoint": {
|
||||
"label": "Endpoint Address",
|
||||
"placeholder": "The address of the remote endpoint"
|
||||
},
|
||||
"ip": {
|
||||
"label": "IP Addresses",
|
||||
"placeholder": "IP Addresses (CIDR format)"
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Allowed IP Addresses",
|
||||
"placeholder": "Allowed IP Addresses (CIDR format)"
|
||||
},
|
||||
"extra-allowed-ip": {
|
||||
"label": "Extra allowed IP Addresses",
|
||||
"placeholder": "Extra allowed IP's (Server Sided)",
|
||||
"description": "Those IP's will be added on the remote WireGuard interface as allowed IP's."
|
||||
},
|
||||
"dns": {
|
||||
"label": "DNS Server",
|
||||
"placeholder": "The DNS servers that should be used"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "DNS Search Domains",
|
||||
"placeholder": "DNS search prefixes"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Keep Alive Interval",
|
||||
"placeholder": "Persistent Keepalive (0 = default)"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The client MTU (0 = keep default)"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pre-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pre-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Peer Disabled"
|
||||
},
|
||||
"ignore-global": {
|
||||
"label": "Ignore global settings"
|
||||
},
|
||||
"expires-at": {
|
||||
"label": "Expiry date"
|
||||
}
|
||||
},
|
||||
"peer-multi-create": {
|
||||
"headline-peer": "Create multiple peers",
|
||||
"headline-endpoint": "Create multiple endpoints",
|
||||
"identifiers": {
|
||||
"label": "User Identifiers",
|
||||
"placeholder": "User Identifiers",
|
||||
"description": "A user identifier (the username) for which a peer should be created."
|
||||
},
|
||||
"prefix": {
|
||||
"headline-peer": "Peer:",
|
||||
"headline-endpoint": "Endpoint:",
|
||||
"label": "Display Name Prefix",
|
||||
"placeholder": "The prefix",
|
||||
"description": "A prefix that is added to the peers display name."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
45
frontend/src/main.js
Normal file
45
frontend/src/main.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
|
||||
import i18n from "./lang";
|
||||
|
||||
import Notifications from '@kyvg/vue3-notification'
|
||||
|
||||
// Bootstrap (and theme)
|
||||
//import "bootstrap/dist/css/bootstrap.min.css"
|
||||
import "bootswatch/dist/lux/bootstrap.min.css";
|
||||
import "bootstrap";
|
||||
import "./assets/base.css";
|
||||
|
||||
// Fontawesome
|
||||
import "@fortawesome/fontawesome-free/js/all.js"
|
||||
|
||||
// Flags
|
||||
import "flag-icons/css/flag-icons.min.css"
|
||||
|
||||
// Syntax Highlighting
|
||||
import 'prismjs'
|
||||
import 'prismjs/themes/prism-okaidia.css'
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(i18n)
|
||||
app.use(createPinia());
|
||||
app.use(router);
|
||||
app.use(Notifications);
|
||||
|
||||
app.config.globalProperties.$filters = {
|
||||
truncate(value, maxLength, suffix) {
|
||||
suffix = suffix || '...'
|
||||
if (value.length > maxLength) {
|
||||
return value.substring(0, maxLength) + suffix;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.mount("#app");
|
109
frontend/src/router/index.js
Normal file
109
frontend/src/router/index.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import {createRouter, createWebHashHistory} from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import LoginView from '../views/LoginView.vue'
|
||||
import InterfaceView from '../views/InterfaceView.vue'
|
||||
|
||||
import {authStore} from '@/stores/auth'
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: LoginView
|
||||
},
|
||||
{
|
||||
path: '/interface',
|
||||
name: 'interface',
|
||||
component: InterfaceView
|
||||
},
|
||||
{
|
||||
path: '/interfaces',
|
||||
name: 'interfaces',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/InterfaceView.vue')
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'users',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/UserView.vue')
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'profile',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/ProfileView.vue')
|
||||
}
|
||||
],
|
||||
linkActiveClass: "active",
|
||||
linkExactActiveClass: "exact-active",
|
||||
})
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
const auth = authStore()
|
||||
|
||||
// check if the request was a successful oauth login
|
||||
if ('wgLoginState' in to.query && !auth.IsAuthenticated) {
|
||||
const state = to.query['wgLoginState']
|
||||
const returnUrl = auth.ReturnUrl
|
||||
console.log("Oauth login callback:", state)
|
||||
|
||||
if (state === "success") {
|
||||
try {
|
||||
const uid = await auth.LoadSession()
|
||||
console.log("Oauth login completed for UID:", uid)
|
||||
console.log("Continuing to:", returnUrl)
|
||||
|
||||
notify({
|
||||
title: "Logged in",
|
||||
text: "Authentication suceeded!",
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
auth.ResetReturnUrl()
|
||||
return returnUrl
|
||||
} catch (e) {
|
||||
notify({
|
||||
title: "Login failed!",
|
||||
text: "Oauth session is invalid!",
|
||||
type: 'error',
|
||||
})
|
||||
|
||||
return '/login'
|
||||
}
|
||||
} else {
|
||||
notify({
|
||||
title: "Login failed!",
|
||||
text: "Authentication via Oauth failed!",
|
||||
type: 'error',
|
||||
})
|
||||
|
||||
return '/login'
|
||||
}
|
||||
}
|
||||
|
||||
// redirect to login page if not logged in and trying to access a restricted page
|
||||
const publicPages = ['/', '/login']
|
||||
const authRequired = !publicPages.includes(to.path)
|
||||
|
||||
if (authRequired && !auth.IsAuthenticated) {
|
||||
auth.SetReturnUrl(to.fullPath) // store original destination before starting the auth process
|
||||
return '/login'
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
125
frontend/src/stores/auth.js
Normal file
125
frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||
import router from '../router'
|
||||
|
||||
export const authStore = defineStore({
|
||||
id: 'auth',
|
||||
state: () => ({
|
||||
// initialize state from local storage to enable user to stay logged in
|
||||
user: JSON.parse(localStorage.getItem('user')),
|
||||
providers: [],
|
||||
returnUrl: localStorage.getItem('returnUrl')
|
||||
}),
|
||||
getters: {
|
||||
UserIdentifier: (state) => state.user?.Identifier || 'unknown',
|
||||
User: (state) => state.user,
|
||||
LoginProviders: (state) => state.providers,
|
||||
IsAuthenticated: (state) => state.user != null,
|
||||
IsAdmin: (state) => state.user?.IsAdmin || false,
|
||||
ReturnUrl: (state) => state.returnUrl || '/',
|
||||
},
|
||||
actions: {
|
||||
SetReturnUrl(link) {
|
||||
this.returnUrl = link
|
||||
localStorage.setItem('returnUrl', link)
|
||||
},
|
||||
ResetReturnUrl() {
|
||||
this.returnUrl = null
|
||||
localStorage.removeItem('returnUrl')
|
||||
},
|
||||
// LoadProviders always returns a fulfilled promise, even if the request failed.
|
||||
async LoadProviders() {
|
||||
apiWrapper.get(`/auth/providers`)
|
||||
.then(providers => this.providers = providers)
|
||||
.catch(error => {
|
||||
this.providers = []
|
||||
console.log("Failed to load auth providers: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load external authentication providers!",
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// LoadSession returns promise that might have been rejected if the session was not authenticated.
|
||||
async LoadSession() {
|
||||
return apiWrapper.get(`/auth/session`)
|
||||
.then(session => {
|
||||
if (session.LoggedIn === true) {
|
||||
this.ResetReturnUrl()
|
||||
this.setUserInfo(session)
|
||||
return session.UserIdentifier
|
||||
} else {
|
||||
this.setUserInfo(null)
|
||||
return Promise.reject(new Error('session not authenticated'))
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.setUserInfo(null)
|
||||
return Promise.reject(err)
|
||||
})
|
||||
},
|
||||
// Login returns promise that might have been rejected if the login attempt was not successful.
|
||||
async Login(username, password) {
|
||||
return apiWrapper.post(`/auth/login`, { username, password })
|
||||
.then(user => {
|
||||
this.ResetReturnUrl()
|
||||
this.setUserInfo(user)
|
||||
return user.Identifier
|
||||
})
|
||||
.catch(err => {
|
||||
console.log("Login failed:", err)
|
||||
this.setUserInfo(null)
|
||||
return Promise.reject(new Error("login failed"))
|
||||
})
|
||||
},
|
||||
async Logout() {
|
||||
this.setUserInfo(null)
|
||||
this.ResetReturnUrl() // just to be sure^^
|
||||
|
||||
try {
|
||||
await apiWrapper.post(`/auth/logout`)
|
||||
} catch (e) {
|
||||
console.log("Logout request failed:", e)
|
||||
}
|
||||
|
||||
notify({
|
||||
title: "Logged Out",
|
||||
text: "Logout successful!",
|
||||
type: "warn",
|
||||
})
|
||||
|
||||
|
||||
await router.push('/login')
|
||||
},
|
||||
// -- internal setters
|
||||
setUserInfo(userInfo) {
|
||||
// store user details and jwt in local storage to keep user logged in between page refreshes
|
||||
if (userInfo) {
|
||||
if ('UserIdentifier' in userInfo) { // session object
|
||||
this.user = {
|
||||
Identifier: userInfo['UserIdentifier'],
|
||||
Firstname: userInfo['UserFirstname'],
|
||||
Lastname: userInfo['UserLastname'],
|
||||
Email: userInfo['UserEmail'],
|
||||
IsAdmin: userInfo['IsAdmin']
|
||||
}
|
||||
} else { // user object
|
||||
this.user = {
|
||||
Identifier: userInfo['Identifier'],
|
||||
Firstname: userInfo['Firstname'],
|
||||
Lastname: userInfo['Lastname'],
|
||||
Email: userInfo['Email'],
|
||||
IsAdmin: userInfo['IsAdmin']
|
||||
}
|
||||
}
|
||||
localStorage.setItem('user', JSON.stringify(this.user))
|
||||
} else {
|
||||
this.user = null
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
152
frontend/src/stores/interfaces.js
Normal file
152
frontend/src/stores/interfaces.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import {apiWrapper} from '@/helpers/fetch-wrapper'
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import { freshInterface } from '@/helpers/models';
|
||||
import { base64_url_encode } from '@/helpers/encoding';
|
||||
|
||||
const baseUrl = `/interface`
|
||||
|
||||
export const interfaceStore = defineStore({
|
||||
id: 'interfaces',
|
||||
state: () => ({
|
||||
interfaces: [],
|
||||
prepared: freshInterface(),
|
||||
configuration: "",
|
||||
selected: "",
|
||||
fetching: false,
|
||||
}),
|
||||
getters: {
|
||||
Count: (state) => state.interfaces.length,
|
||||
Prepared: (state) => {console.log("STATE:", state.prepared); return state.prepared},
|
||||
All: (state) => state.interfaces,
|
||||
Find: (state) => {
|
||||
return (id) => state.interfaces.find((p) => p.Identifier === id)
|
||||
},
|
||||
GetSelected: (state) => state.interfaces.find((i) => i.Identifier === state.selected) || state.interfaces[0],
|
||||
isFetching: (state) => state.fetching,
|
||||
},
|
||||
actions: {
|
||||
setInterfaces(interfaces) {
|
||||
this.interfaces = interfaces
|
||||
if (this.interfaces.length > 0) {
|
||||
this.selected = this.interfaces[0].Identifier
|
||||
} else {
|
||||
this.selected = ""
|
||||
}
|
||||
this.fetching = false
|
||||
},
|
||||
async LoadInterfaces() {
|
||||
this.fetching = true
|
||||
return apiWrapper.get(`${baseUrl}/all`)
|
||||
.then(this.setInterfaces)
|
||||
.catch(error => {
|
||||
this.setInterfaces([])
|
||||
console.log("Failed to load interfaces: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load interfaces!",
|
||||
})
|
||||
})
|
||||
},
|
||||
setPreparedInterface(iface) {
|
||||
this.prepared = iface;
|
||||
},
|
||||
setInterfaceConfig(ifaceConfig) {
|
||||
this.configuration = ifaceConfig;
|
||||
},
|
||||
async PrepareInterface() {
|
||||
return apiWrapper.get(`${baseUrl}/prepare`)
|
||||
.then(this.setPreparedInterface)
|
||||
.catch(error => {
|
||||
this.prepared = freshInterface()
|
||||
console.log("Failed to load prepared interface: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load prepared interface!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async LoadInterfaceConfig(id) {
|
||||
return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}`)
|
||||
.then(this.setInterfaceConfig)
|
||||
.catch(error => {
|
||||
this.configuration = ""
|
||||
console.log("Failed to load interface configuration: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load interface configuration!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async DeleteInterface(id) {
|
||||
this.fetching = true
|
||||
return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
|
||||
.then(() => {
|
||||
this.interfaces = this.interfaces.filter(i => i.Identifier !== id)
|
||||
if (this.interfaces.length > 0) {
|
||||
this.selected = this.interfaces[0].Identifier
|
||||
} else {
|
||||
this.selected = ""
|
||||
}
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async UpdateInterface(id, formData) {
|
||||
this.fetching = true
|
||||
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
|
||||
.then(iface => {
|
||||
let idx = this.interfaces.findIndex((i) => i.Identifier === id)
|
||||
this.interfaces[idx] = iface
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async CreateInterface(formData) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/new`, formData)
|
||||
.then(iface => {
|
||||
this.interfaces.push(iface)
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async ApplyPeerDefaults(id, formData) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/apply-peer-defaults`, formData)
|
||||
.then(() => {
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async SaveConfiguration(id) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/save-config`)
|
||||
.then(() => {
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
258
frontend/src/stores/peers.js
Normal file
258
frontend/src/stores/peers.js
Normal file
@@ -0,0 +1,258 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import {apiWrapper} from "@/helpers/fetch-wrapper";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {interfaceStore} from "./interfaces";
|
||||
import {freshPeer, freshStats} from '@/helpers/models';
|
||||
import { base64_url_encode } from '@/helpers/encoding';
|
||||
|
||||
const baseUrl = `/peer`
|
||||
|
||||
export const peerStore = defineStore({
|
||||
id: 'peers',
|
||||
state: () => ({
|
||||
peers: [],
|
||||
stats: {},
|
||||
statsEnabled: false,
|
||||
peer: freshPeer(),
|
||||
prepared: freshPeer(),
|
||||
configuration: "",
|
||||
filter: "",
|
||||
pageSize: 10,
|
||||
pageOffset: 0,
|
||||
pages: [],
|
||||
fetching: false,
|
||||
}),
|
||||
getters: {
|
||||
Find: (state) => {
|
||||
return (id) => state.peers.find((p) => p.Identifier === id)
|
||||
},
|
||||
|
||||
Count: (state) => state.peers.length,
|
||||
Prepared: (state) => {console.log("STATE:", state.prepared); return state.prepared},
|
||||
FilteredCount: (state) => state.Filtered.length,
|
||||
All: (state) => state.peers,
|
||||
Filtered: (state) => {
|
||||
if (!state.filter) {
|
||||
return state.peers
|
||||
}
|
||||
return state.peers.filter((p) => {
|
||||
return p.DisplayName.includes(state.filter) || p.Identifier.includes(state.filter)
|
||||
})
|
||||
},
|
||||
FilteredAndPaged: (state) => {
|
||||
return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
|
||||
},
|
||||
ConfigQrUrl: (state) => {
|
||||
return (id) => state.peers.find((p) => p.Identifier === id) ? apiWrapper.url(`${baseUrl}/config-qr/${base64_url_encode(id)}`) : ''
|
||||
},
|
||||
isFetching: (state) => state.fetching,
|
||||
hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
|
||||
hasPrevPage: (state) => state.pageOffset > 0,
|
||||
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
|
||||
Statistics: (state) => {
|
||||
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
||||
},
|
||||
hasStatistics: (state) => state.statsEnabled,
|
||||
|
||||
},
|
||||
actions: {
|
||||
afterPageSizeChange() {
|
||||
// reset pageOffset to avoid problems with new page sizes
|
||||
this.pageOffset = 0
|
||||
this.calculatePages()
|
||||
},
|
||||
calculatePages() {
|
||||
let pageCounter = 1;
|
||||
this.pages = []
|
||||
for (let i = 0; i < this.FilteredCount; i+=this.pageSize) {
|
||||
this.pages.push(pageCounter++)
|
||||
}
|
||||
},
|
||||
gotoPage(page) {
|
||||
this.pageOffset = (page-1) * this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
nextPage() {
|
||||
this.pageOffset += this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
previousPage() {
|
||||
this.pageOffset -= this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
setPeers(peers) {
|
||||
this.peers = peers
|
||||
this.calculatePages()
|
||||
this.fetching = false
|
||||
},
|
||||
setPeer(peer) {
|
||||
this.peer = peer
|
||||
this.fetching = false
|
||||
},
|
||||
setPreparedPeer(peer) {
|
||||
this.prepared = peer;
|
||||
},
|
||||
setPeerConfig(config) {
|
||||
this.configuration = config;
|
||||
},
|
||||
setStats(statsResponse) {
|
||||
if (!statsResponse) {
|
||||
this.stats = {}
|
||||
this.statsEnabled = false
|
||||
}
|
||||
this.stats = statsResponse.Stats
|
||||
this.statsEnabled = statsResponse.Enabled
|
||||
},
|
||||
async PreparePeer(interfaceId) {
|
||||
return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/prepare`)
|
||||
.then(this.setPreparedPeer)
|
||||
.catch(error => {
|
||||
this.prepared = freshPeer()
|
||||
console.log("Failed to load prepared peer: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load prepared peer!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async MailPeerConfig(linkOnly, ids) {
|
||||
return apiWrapper.post(`${baseUrl}/config-mail`, {
|
||||
Identifiers: ids,
|
||||
LinkOnly: linkOnly
|
||||
})
|
||||
.then(() => {
|
||||
notify({
|
||||
title: "Peer Configuration sent",
|
||||
text: "Email sent to linked user!",
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("Failed to send peer configuration: ", error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async LoadPeerConfig(id) {
|
||||
return apiWrapper.get(`${baseUrl}/config/${base64_url_encode(id)}`)
|
||||
.then(this.setPeerConfig)
|
||||
.catch(error => {
|
||||
this.configuration = ""
|
||||
console.log("Failed to load peer configuration: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load peer configuration!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async LoadPeer(id) {
|
||||
this.fetching = true
|
||||
return apiWrapper.get(`${baseUrl}/${base64_url_encode(id)}`)
|
||||
.then(this.setPeer)
|
||||
.catch(error => {
|
||||
this.setPeers([])
|
||||
console.log("Failed to load peer: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load peer!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async LoadStats(interfaceId) {
|
||||
// if no interfaceId is given, use the currently selected interface
|
||||
if (!interfaceId) {
|
||||
interfaceId = interfaceStore().GetSelected.Identifier
|
||||
if (!interfaceId) {
|
||||
return // no interface, nothing to load
|
||||
}
|
||||
}
|
||||
this.fetching = true
|
||||
|
||||
return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/stats`)
|
||||
.then(this.setStats)
|
||||
.catch(error => {
|
||||
this.setStats(undefined)
|
||||
console.log("Failed to load peer stats: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load peer stats!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async DeletePeer(id) {
|
||||
this.fetching = true
|
||||
return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
|
||||
.then(() => {
|
||||
this.peers = this.peers.filter(p => p.Identifier !== id)
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async UpdatePeer(id, formData) {
|
||||
this.fetching = true
|
||||
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
|
||||
.then(peer => {
|
||||
let idx = this.peers.findIndex((p) => p.Identifier === id)
|
||||
this.peers[idx] = peer
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async CreatePeer(interfaceId, formData) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/new`, formData)
|
||||
.then(peer => {
|
||||
this.peers.push(peer)
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async CreateMultiplePeers(interfaceId, formData) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/multiplenew`, formData)
|
||||
.then(peers => {
|
||||
this.peers.push(...peers)
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async LoadPeers(interfaceId) {
|
||||
// if no interfaceId is given, use the currently selected interface
|
||||
if (!interfaceId) {
|
||||
interfaceId = interfaceStore().GetSelected.Identifier
|
||||
if (!interfaceId) {
|
||||
return // no interface, nothing to load
|
||||
}
|
||||
}
|
||||
this.fetching = true
|
||||
|
||||
return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/all`)
|
||||
.then(this.setPeers)
|
||||
.catch(error => {
|
||||
this.setPeers([])
|
||||
console.log("Failed to load peers: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load peers!",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
137
frontend/src/stores/profile.js
Normal file
137
frontend/src/stores/profile.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import {apiWrapper} from "@/helpers/fetch-wrapper";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {authStore} from "@/stores/auth";
|
||||
import { base64_url_encode } from '@/helpers/encoding';
|
||||
import {freshStats} from "@/helpers/models";
|
||||
|
||||
const baseUrl = `/user`
|
||||
|
||||
export const profileStore = defineStore({
|
||||
id: 'profile',
|
||||
state: () => ({
|
||||
peers: [],
|
||||
stats: {},
|
||||
statsEnabled: false,
|
||||
user: {},
|
||||
filter: "",
|
||||
pageSize: 10,
|
||||
pageOffset: 0,
|
||||
pages: [],
|
||||
fetching: false,
|
||||
}),
|
||||
getters: {
|
||||
FindPeers: (state) => {
|
||||
return (id) => state.peers.find((p) => p.Identifier === id)
|
||||
},
|
||||
CountPeers: (state) => state.peers.length,
|
||||
FilteredPeerCount: (state) => state.FilteredPeers.length,
|
||||
Peers: (state) => state.peers,
|
||||
FilteredPeers: (state) => {
|
||||
if (!state.filter) {
|
||||
return state.peers
|
||||
}
|
||||
return state.peers.filter((p) => {
|
||||
return p.DisplayName.includes(state.filter) || p.Identifier.includes(state.filter)
|
||||
})
|
||||
},
|
||||
FilteredAndPagedPeers: (state) => {
|
||||
return state.FilteredPeers.slice(state.pageOffset, state.pageOffset + state.pageSize)
|
||||
},
|
||||
isFetching: (state) => state.fetching,
|
||||
hasNextPage: (state) => state.pageOffset < (state.FilteredPeerCount - state.pageSize),
|
||||
hasPrevPage: (state) => state.pageOffset > 0,
|
||||
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
|
||||
Statistics: (state) => {
|
||||
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
||||
},
|
||||
hasStatistics: (state) => state.statsEnabled,
|
||||
},
|
||||
actions: {
|
||||
afterPageSizeChange() {
|
||||
// reset pageOffset to avoid problems with new page sizes
|
||||
this.pageOffset = 0
|
||||
this.calculatePages()
|
||||
},
|
||||
calculatePages() {
|
||||
let pageCounter = 1;
|
||||
this.pages = []
|
||||
for (let i = 0; i < this.FilteredPeerCount; i+=this.pageSize) {
|
||||
this.pages.push(pageCounter++)
|
||||
}
|
||||
},
|
||||
gotoPage(page) {
|
||||
this.pageOffset = (page-1) * this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
nextPage() {
|
||||
this.pageOffset += this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
previousPage() {
|
||||
this.pageOffset -= this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
setPeers(peers) {
|
||||
this.peers = peers
|
||||
this.fetching = false
|
||||
},
|
||||
setUser(user) {
|
||||
this.user = user
|
||||
this.fetching = false
|
||||
},
|
||||
setStats(statsResponse) {
|
||||
if (!statsResponse) {
|
||||
this.stats = {}
|
||||
this.statsEnabled = false
|
||||
}
|
||||
this.stats = statsResponse.Stats
|
||||
this.statsEnabled = statsResponse.Enabled
|
||||
},
|
||||
async LoadPeers() {
|
||||
this.fetching = true
|
||||
let currentUser = authStore().user.Identifier
|
||||
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}/peers`)
|
||||
.then(this.setPeers)
|
||||
.catch(error => {
|
||||
this.setPeers([])
|
||||
console.log("Failed to load user peers for ", currentUser, ": ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load user peers!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async LoadStats() {
|
||||
this.fetching = true
|
||||
let currentUser = authStore().user.Identifier
|
||||
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}/stats`)
|
||||
.then(this.setStats)
|
||||
.catch(error => {
|
||||
this.setStats(undefined)
|
||||
console.log("Failed to load peer stats: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load peer stats!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async LoadUser() {
|
||||
this.fetching = true
|
||||
let currentUser = authStore().user.Identifier
|
||||
return apiWrapper.get(`${baseUrl}/${base64_url_encode(currentUser)}`)
|
||||
.then(this.setUser)
|
||||
.catch(error => {
|
||||
this.setUser({})
|
||||
console.log("Failed to load user for ", currentUser, ": ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load user!",
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
32
frontend/src/stores/security.js
Normal file
32
frontend/src/stores/security.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||
|
||||
export const securityStore = defineStore({
|
||||
id: 'security',
|
||||
state: () => ({
|
||||
csrfToken: "",
|
||||
}),
|
||||
getters: {
|
||||
CsrfToken: (state) => state.csrfToken,
|
||||
},
|
||||
actions: {
|
||||
SetCsrfToken(token) {
|
||||
this.csrfToken = token
|
||||
},
|
||||
// LoadSecurityProperties always returns a fulfilled promise, even if the request failed.
|
||||
async LoadSecurityProperties() {
|
||||
await apiWrapper.get(`/csrf`)
|
||||
.then(token => this.SetCsrfToken(token))
|
||||
.catch(error => {
|
||||
this.SetCsrfToken("");
|
||||
console.log("Failed to load csrf token: ", error);
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load csrf token!",
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
36
frontend/src/stores/settings.js
Normal file
36
frontend/src/stores/settings.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||
|
||||
const baseUrl = `/config`
|
||||
|
||||
export const settingsStore = defineStore({
|
||||
id: 'settings',
|
||||
state: () => ({
|
||||
settings: {},
|
||||
}),
|
||||
getters: {
|
||||
Setting: (state) => {
|
||||
return (key) => (key in state.settings) ? state.settings[key] : undefined
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setSettings(settings) {
|
||||
this.settings = settings
|
||||
},
|
||||
// LoadSecurityProperties always returns a fulfilled promise, even if the request failed.
|
||||
async LoadSettings() {
|
||||
await apiWrapper.get(`${baseUrl}/settings`)
|
||||
.then(data => this.setSettings(data))
|
||||
.catch(error => {
|
||||
this.setSettings({});
|
||||
console.log("Failed to load settings: ", error);
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load settings!",
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
147
frontend/src/stores/users.js
Normal file
147
frontend/src/stores/users.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import {apiWrapper} from "@/helpers/fetch-wrapper";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import { base64_url_encode } from '@/helpers/encoding';
|
||||
|
||||
const baseUrl = `/user`
|
||||
|
||||
export const userStore = defineStore({
|
||||
id: 'users',
|
||||
state: () => ({
|
||||
userPeers: [],
|
||||
users: [],
|
||||
filter: "",
|
||||
pageSize: 10,
|
||||
pageOffset: 0,
|
||||
pages: [],
|
||||
fetching: false,
|
||||
}),
|
||||
getters: {
|
||||
Find: (state) => {
|
||||
return (id) => state.users.find((p) => p.Identifier === id)
|
||||
},
|
||||
Count: (state) => state.users.length,
|
||||
FilteredCount: (state) => state.Filtered.length,
|
||||
All: (state) => state.users,
|
||||
Peers: (state) => state.userPeers,
|
||||
Filtered: (state) => {
|
||||
if (!state.filter) {
|
||||
return state.users
|
||||
}
|
||||
return state.users.filter((u) => {
|
||||
return u.Firstname.includes(state.filter) || u.Lastname.includes(state.filter) || u.Email.includes(state.filter) || u.Identifier.includes(state.filter)
|
||||
})
|
||||
},
|
||||
FilteredAndPaged: (state) => {
|
||||
return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
|
||||
},
|
||||
isFetching: (state) => state.fetching,
|
||||
hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
|
||||
hasPrevPage: (state) => state.pageOffset > 0,
|
||||
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
|
||||
},
|
||||
actions: {
|
||||
afterPageSizeChange() {
|
||||
// reset pageOffset to avoid problems with new page sizes
|
||||
this.pageOffset = 0
|
||||
this.calculatePages()
|
||||
},
|
||||
calculatePages() {
|
||||
let pageCounter = 1;
|
||||
this.pages = []
|
||||
for (let i = 0; i < this.FilteredCount; i+=this.pageSize) {
|
||||
this.pages.push(pageCounter++)
|
||||
}
|
||||
},
|
||||
gotoPage(page) {
|
||||
this.pageOffset = (page-1) * this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
nextPage() {
|
||||
this.pageOffset += this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
previousPage() {
|
||||
this.pageOffset -= this.pageSize
|
||||
|
||||
this.calculatePages()
|
||||
},
|
||||
setUsers(users) {
|
||||
this.users = users
|
||||
this.calculatePages()
|
||||
this.fetching = false
|
||||
},
|
||||
setUserPeers(peers) {
|
||||
this.userPeers = peers
|
||||
this.fetching = false
|
||||
},
|
||||
async LoadUsers() {
|
||||
this.fetching = true
|
||||
return apiWrapper.get(`${baseUrl}/all`)
|
||||
.then(this.setUsers)
|
||||
.catch(error => {
|
||||
this.setUsers([])
|
||||
console.log("Failed to load users: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load users!",
|
||||
})
|
||||
})
|
||||
},
|
||||
async DeleteUser(id) {
|
||||
this.fetching = true
|
||||
return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
|
||||
.then(() => {
|
||||
this.users = this.users.filter(u => u.Identifier !== id)
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async UpdateUser(id, formData) {
|
||||
this.fetching = true
|
||||
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
|
||||
.then(user => {
|
||||
let idx = this.users.findIndex((u) => u.Identifier === id)
|
||||
this.users[idx] = user
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async CreateUser(formData) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/new`, formData)
|
||||
.then(user => {
|
||||
this.users.push(user)
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log(error)
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async LoadUserPeers(id) {
|
||||
this.fetching = true
|
||||
return apiWrapper.get(`${baseUrl}/${base64_url_encode(id)}/peers`)
|
||||
.then(this.setUserPeers)
|
||||
.catch(error => {
|
||||
this.setUserPeers([])
|
||||
console.log("Failed to load user peers for ",id ,": ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to load user peers!",
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
73
frontend/src/views/HomeView.vue
Normal file
73
frontend/src/views/HomeView.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import {authStore} from "@/stores/auth";
|
||||
import {RouterLink} from "vue-router";
|
||||
|
||||
const auth = authStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<h1>{{ $t('home.headline') }}</h1>
|
||||
</div>
|
||||
|
||||
<p class="lead">{{ $t('home.abstract') }}</p>
|
||||
|
||||
|
||||
<div class="bg-light p-5" v-if="auth.IsAuthenticated">
|
||||
<h2 class="display-5">{{ $t('home.profiles.headline') }}</h2>
|
||||
<p class="lead">{{ $t('home.profiles.abstract') }}</p>
|
||||
<hr class="my-4">
|
||||
<p>{{ $t('home.profiles.content') }}</p>
|
||||
<p class="lead">
|
||||
<RouterLink :to="{ name: 'profile' }" class="btn btn-primary btn-lg">{{ $t('home.profiles.button') }}</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-light p-5 mt-4" v-if="auth.IsAuthenticated && auth.IsAdmin">
|
||||
<h2 class="display-5">{{ $t('home.admin.headline') }}</h2>
|
||||
<p class="lead">{{ $t('home.admin.abstract') }}</p>
|
||||
<hr class="my-4">
|
||||
<p>{{ $t('home.admin.content') }}</p>
|
||||
<p class="lead">
|
||||
<RouterLink :to="{ name: 'interfaces' }" class="btn btn-primary btn-lg me-2">{{ $t('home.admin.button-admin') }}</RouterLink>
|
||||
<RouterLink :to="{ name: 'users' }" class="btn btn-primary btn-lg">{{ $t('home.admin.button-user') }}</RouterLink>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-5">{{ $t('home.info-headline') }}</h3>
|
||||
<div class="row">
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-secondary mb-4" style="min-height: 15rem;">
|
||||
<div class="card-header">{{ $t('home.installation.box-header') }}</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h4 class="card-title">{{ $t('home.installation.headline') }}</h4>
|
||||
<p class="card-text">{{ $t('home.installation.content') }}</p>
|
||||
<a href="https://www.wireguard.com/install/" title="WireGuard Installation" target="_blank"
|
||||
rel="noopener noreferrer" class="mt-auto btn btn-primary btn-sm">{{ $t('home.installation.button') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-secondary mb-4" style="min-height: 15rem;">
|
||||
<div class="card-header">{{ $t('home.about-wg.box-header') }}</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h4 class="card-title">{{ $t('home.about-wg.headline') }}</h4>
|
||||
<p class="card-text">{{ $t('home.about-wg.content') }}</p>
|
||||
<a href="https://www.wireguard.com/" title="WireGuard" target="_blank" rel="noopener noreferrer"
|
||||
class="mt-auto btn btn-primary btn-sm">{{ $t('home.about-wg.button') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-secondary mb-4" style="min-height: 15rem;">
|
||||
<div class="card-header">{{ $t('home.about-portal.box-header') }}</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h4 class="card-title">{{ $t('home.about-portal.headline') }}</h4>
|
||||
<p class="card-text">{{ $t('home.about-portal.content') }}</p>
|
||||
<a href="https://github.com/h44z/wg-portal/" title="WireGuard Portal" target="_blank"
|
||||
rel="noopener noreferrer" class="mt-auto btn btn-primary btn-sm">{{ $t('home.about-portal.button') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
390
frontend/src/views/InterfaceView.vue
Normal file
390
frontend/src/views/InterfaceView.vue
Normal file
@@ -0,0 +1,390 @@
|
||||
<script setup>
|
||||
import PeerViewModal from "../components/PeerViewModal.vue";
|
||||
import PeerEditModal from "../components/PeerEditModal.vue";
|
||||
import PeerMultiCreateModal from "../components/PeerMultiCreateModal.vue";
|
||||
import InterfaceEditModal from "../components/InterfaceEditModal.vue";
|
||||
import InterfaceViewModal from "../components/InterfaceViewModal.vue";
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import {peerStore} from "@/stores/peers";
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
|
||||
const settings = settingsStore()
|
||||
const interfaces = interfaceStore()
|
||||
const peers = peerStore()
|
||||
|
||||
const viewedPeerId = ref("")
|
||||
const editPeerId = ref("")
|
||||
const multiCreatePeerId = ref("")
|
||||
const editInterfaceId = ref("")
|
||||
const viewedInterfaceId = ref("")
|
||||
|
||||
function calculateInterfaceName(id, name) {
|
||||
let result = id
|
||||
if (name) {
|
||||
result += ' (' + name + ')'
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function download() {
|
||||
await interfaces.LoadInterfaceConfig(interfaces.GetSelected.Identifier)
|
||||
|
||||
// credit: https://www.bitdegree.org/learn/javascript-download
|
||||
let filename = interfaces.GetSelected.Identifier + ".conf"
|
||||
let text = interfaces.configuration
|
||||
|
||||
let element = document.createElement('a')
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
|
||||
element.setAttribute('download', filename)
|
||||
|
||||
element.style.display = 'none'
|
||||
document.body.appendChild(element)
|
||||
|
||||
element.click()
|
||||
document.body.removeChild(element)
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
try {
|
||||
await interfaces.SaveConfiguration(interfaces.GetSelected.Identifier)
|
||||
|
||||
notify({
|
||||
title: "Interface configuration persisted to file",
|
||||
text: "The interface configuration has been written to the wg-quick configuration file.",
|
||||
type: 'success',
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to persist interface configuration file!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await interfaces.LoadInterfaces()
|
||||
await peers.LoadPeers(undefined) // use default interface
|
||||
await peers.LoadStats(undefined) // use default interface
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId!==''" @close="viewedPeerId=''"></PeerViewModal>
|
||||
<PeerEditModal :peerId="editPeerId" :visible="editPeerId!==''" @close="editPeerId=''"></PeerEditModal>
|
||||
<PeerMultiCreateModal :visible="multiCreatePeerId!==''" @close="multiCreatePeerId=''"></PeerMultiCreateModal>
|
||||
<InterfaceEditModal :interfaceId="editInterfaceId" :visible="editInterfaceId!==''" @close="editInterfaceId=''"></InterfaceEditModal>
|
||||
<InterfaceViewModal :interfaceId="viewedInterfaceId" :visible="viewedInterfaceId!==''" @close="viewedInterfaceId=''"></InterfaceViewModal>
|
||||
|
||||
<!-- Headline and interface selector -->
|
||||
<div class="page-header row">
|
||||
<div class="col-12 col-lg-8">
|
||||
<h1>{{ $t('interfaces.headline') }}</h1>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 text-end">
|
||||
<div class="form-group">
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group mb-3">
|
||||
<button class="input-group-text btn btn-primary" :title="$t('interfaces.button-add-interface')" @click.prevent="editInterfaceId='#NEW#'">
|
||||
<i class="fa-solid fa-plus-circle"></i>
|
||||
</button>
|
||||
<select v-model="interfaces.selected" :disabled="interfaces.Count===0" class="form-select" @change="peers.LoadPeers()">
|
||||
<option v-if="interfaces.Count===0" value="nothing">{{ $t('interfaces.no-interface.default-selection') }}</option>
|
||||
<option v-for="iface in interfaces.All" :key="iface.Identifier" :value="iface.Identifier">{{ calculateInterfaceName(iface.Identifier,iface.DisplayName) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No interfaces information -->
|
||||
<div v-if="interfaces.Count===0" class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="mt-5">
|
||||
<h4>{{ $t('interfaces.no-interface.headline') }}</h4>
|
||||
<p>{{ $t('interfaces.no-interface.abstract') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interface overview -->
|
||||
<div v-if="interfaces.Count!==0" class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="card border-secondary mb-4" style="min-height: 15rem;">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-8">
|
||||
{{ $t('interfaces.interface.headline') }} <strong>{{interfaces.GetSelected.Identifier}}</strong> ({{interfaces.GetSelected.Mode}} {{ $t('interfaces.interface.mode') }})
|
||||
<span v-if="interfaces.GetSelected.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="interfaces.GetSelected.DisabledReason"></i></span>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 text-lg-end">
|
||||
<a class="btn-link" href="#" :title="$t('interfaces.interface.button-show-config')" @click.prevent="viewedInterfaceId=interfaces.GetSelected.Identifier"><i class="fas fa-eye"></i></a>
|
||||
<a class="ms-5 btn-link" href="#" :title="$t('interfaces.interface.button-download-config')" @click.prevent="download"><i class="fas fa-download"></i></a>
|
||||
<a v-if="settings.Setting('PersistentConfigSupported')" class="ms-5 btn-link" href="#" :title="$t('interfaces.interface.button-store-config')" @click.prevent="saveConfig"><i class="fas fa-save"></i></a>
|
||||
<a class="ms-5 btn-link" href="#" :title="$t('interfaces.interface.button-edit')" @click.prevent="editInterfaceId=interfaces.GetSelected.Identifier"><i class="fas fa-cog"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div v-if="interfaces.GetSelected.Mode==='server'" class="row">
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.key') }}:</td>
|
||||
<td>{{interfaces.GetSelected.PublicKey}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.endpoint') }}:</td>
|
||||
<td>{{interfaces.GetSelected.PeerDefEndpoint}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.port') }}:</td>
|
||||
<td>{{interfaces.GetSelected.ListenPort}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.peers') }}:</td>
|
||||
<td>{{interfaces.GetSelected.EnabledPeers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.total-peers') }}:</td>
|
||||
<td>{{interfaces.GetSelected.TotalPeers}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.ip') }}:</td>
|
||||
<td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.Addresses" :key="addr">{{addr}}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.dns') }}:</td>
|
||||
<td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.Dns" :key="addr">{{addr}}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.mtu') }}:</td>
|
||||
<td>{{interfaces.GetSelected.Mtu}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.default-keep-alive') }}:</td>
|
||||
<td>{{interfaces.GetSelected.PeerDefPersistentKeepalive}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.default-allowed-ip') }}:</td>
|
||||
<td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.PeerDefAllowedIPs" :key="addr">{{addr}}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="interfaces.GetSelected.Mode==='client'" class="row">
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.key') }}:</td>
|
||||
<td>{{interfaces.GetSelected.PublicKey}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.endpoints') }}:</td>
|
||||
<td>{{interfaces.GetSelected.EnabledPeers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.total-endpoints') }}:</td>
|
||||
<td>{{interfaces.GetSelected.TotalPeers}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.ip') }}:</td>
|
||||
<td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.Addresses" :key="addr">{{addr}}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.dns') }}:</td>
|
||||
<td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.Dns" :key="addr">{{addr}}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.mtu') }}:</td>
|
||||
<td>{{interfaces.GetSelected.Mtu}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="interfaces.GetSelected.Mode==='any'" class="row">
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.key') }}:</td>
|
||||
<td>{{interfaces.GetSelected.PublicKey}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.endpoint') }}:</td>
|
||||
<td>{{interfaces.GetSelected.PeerDefEndpoint}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.port') }}:</td>
|
||||
<td>{{interfaces.GetSelected.ListenPort}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.peers') }}:</td>
|
||||
<td>{{interfaces.GetSelected.EnabledPeers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.total-peers') }}:</td>
|
||||
<td>{{interfaces.GetSelected.TotalPeers}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.ip') }}:</td>
|
||||
<td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.Addresses" :key="addr">{{addr}}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.default-allowed-ip') }}:</td>
|
||||
<td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.PeerDefAllowedIPs" :key="addr">{{addr}}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.dns') }}:</td>
|
||||
<td><span class="badge bg-light me-1" v-for="addr in interfaces.GetSelected.PeerDefDns" :key="addr">{{addr}}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.mtu') }}:</td>
|
||||
<td>{{interfaces.GetSelected.Mtu}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('interfaces.interface.default-keep-alive') }}:</td>
|
||||
<td>{{interfaces.GetSelected.PeerDefPersistentKeepalive}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Peer list -->
|
||||
<div v-if="interfaces.Count!==0" class="mt-4 row">
|
||||
<div class="col-12 col-lg-5">
|
||||
<h2 v-if="interfaces.GetSelected.Mode==='server'" class="mt-2">{{ $t('interfaces.headline-peers') }}</h2>
|
||||
<h2 v-else class="mt-2">{{ $t('interfaces.headline-endpoints') }}</h2>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 text-lg-end">
|
||||
<div class="form-group d-inline">
|
||||
<div class="input-group mb-3">
|
||||
<input v-model="peers.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="peers.afterPageSizeChange">
|
||||
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 text-lg-end">
|
||||
<a class="btn btn-primary ms-2" href="#" :title="$t('interfaces.button-add-peers')" @click.prevent="multiCreatePeerId='#NEW#'"><i class="fa fa-plus me-1"></i><i class="fa fa-users"></i></a>
|
||||
<a class="btn btn-primary ms-2" href="#" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId='#NEW#'"><i class="fa fa-plus me-1"></i><i class="fa fa-user"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="interfaces.Count!==0" class="mt-2 table-responsive">
|
||||
<div v-if="peers.Count===0">
|
||||
<h4>{{ $t('interfaces.no-peer.headline') }}</h4>
|
||||
<p>{{ $t('interfaces.no-peer.abstract') }}</p>
|
||||
</div>
|
||||
<table v-if="peers.Count!==0" id="peerTable" class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<input id="flexCheckDefault" class="form-check-input" :title="$t('general.select-all')" type="checkbox" value="">
|
||||
</th><!-- select -->
|
||||
<th scope="col"></th><!-- status -->
|
||||
<th scope="col">{{ $t('interfaces.table-heading.name') }}</th>
|
||||
<th scope="col">{{ $t('interfaces.table-heading.user') }}</th>
|
||||
<th scope="col">{{ $t('interfaces.table-heading.ip') }}</th>
|
||||
<th v-if="interfaces.GetSelected.Mode==='client'" scope="col">{{ $t('interfaces.table-heading.endpoint') }}</th>
|
||||
<th v-if="peers.hasStatistics" scope="col">{{ $t('interfaces.table-heading.status') }}</th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="peer in peers.FilteredAndPaged" :key="peer.Identifier">
|
||||
<th scope="row">
|
||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
||||
</th>
|
||||
<td class="text-center">
|
||||
<span v-if="peer.Disabled" class="text-danger" :title="$t('interfaces.peer-disabled') + ' ' + peer.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
||||
<span v-if="!peer.Disabled && peer.ExpiresAt" class="text-warning" :title="$t('interfaces.peer-expiring') + ' ' + peer.ExpiresAt"><i class="fas fa-hourglass-end expiring-peer"></i></span>
|
||||
</td>
|
||||
<td><span v-if="peer.DisplayName" :title="peer.Identifier">{{peer.DisplayName}}</span><span v-else :title="peer.Identifier">{{ $filters.truncate(peer.Identifier, 10)}}</span></td>
|
||||
<td>{{peer.UserIdentifier}}</td>
|
||||
<td>
|
||||
<span v-for="ip in peer.Addresses" :key="ip" class="badge bg-light me-1">{{ ip }}</span>
|
||||
</td>
|
||||
<td v-if="interfaces.GetSelected.Mode==='client'">{{peer.Endpoint.Value}}</td>
|
||||
<td v-if="peers.hasStatistics">
|
||||
<div v-if="peers.Statistics(peer.Identifier).IsConnected">
|
||||
<span class="badge rounded-pill bg-success" :title="$t('interfaces.peer-connected')"><i class="fa-solid fa-link"></i></span> <span :title="$t('interfaces.peer-handshake') + ' ' + peers.Statistics(peer.Identifier).LastHandshake">{{ $t('interfaces.peer-connected') }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="badge rounded-pill bg-light" :title="$t('interfaces.peer-not-connected')"><i class="fa-solid fa-link-slash"></i></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="#" :title="$t('interfaces.button-show-peer')" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
||||
<a href="#" :title="$t('interfaces.button-edit-peer')" @click.prevent="editPeerId=peer.Identifier"><i class="fas fa-cog"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<hr v-if="interfaces.Count!==0">
|
||||
<div v-if="interfaces.Count!==0" class="mt-3">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<ul class="pagination pagination-sm">
|
||||
<li :class="{disabled:peers.pageOffset===0}" class="page-item">
|
||||
<a class="page-link" @click="peers.previousPage">«</a>
|
||||
</li>
|
||||
|
||||
<li v-for="page in peers.pages" :key="page" :class="{active:peers.currentPage===page}" class="page-item">
|
||||
<a class="page-link" @click="peers.gotoPage(page)">{{page}}</a>
|
||||
</li>
|
||||
|
||||
<li :class="{disabled:!peers.hasNextPage}" class="page-item">
|
||||
<a class="page-link" @click="peers.nextPage">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||
<div class="col-sm-6">
|
||||
<select v-model.number="peers.pageSize" class="form-select" @click="peers.afterPageSizeChange()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
113
frontend/src/views/LoginView.vue
Normal file
113
frontend/src/views/LoginView.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup>
|
||||
|
||||
import {computed, ref} from "vue";
|
||||
import {authStore} from "@/stores/auth";
|
||||
import router from '../router/index.js'
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
|
||||
const auth = authStore()
|
||||
const settings = settingsStore()
|
||||
|
||||
const loggingIn = ref(false)
|
||||
const username = ref("")
|
||||
const password = ref("")
|
||||
|
||||
const usernameInvalid = computed(() => username.value === "")
|
||||
const passwordInvalid = computed(() => password.value === "")
|
||||
const disableLoginBtn = computed(() => username.value === "" || password.value === "" || loggingIn.value)
|
||||
|
||||
const login = async function () {
|
||||
console.log("Performing login for user:", username.value);
|
||||
loggingIn.value = true;
|
||||
auth.Login(username.value, password.value)
|
||||
.then(uid => {
|
||||
notify({
|
||||
title: "Logged in",
|
||||
text: "Authentication succeeded!",
|
||||
type: 'success',
|
||||
});
|
||||
loggingIn.value = false;
|
||||
settings.LoadSettings(); // only logs errors, does not throw
|
||||
router.push(auth.ReturnUrl);
|
||||
})
|
||||
.catch(error => {
|
||||
notify({
|
||||
title: "Login failed!",
|
||||
text: "Authentication failed!",
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
//loggingIn.value = false;
|
||||
// delay the user from logging in for a short amount of time
|
||||
setTimeout(() => loggingIn.value = false, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const externalLogin = function (provider) {
|
||||
console.log("Performing external login for provider", provider.Identifier);
|
||||
loggingIn.value = true;
|
||||
console.log(router.currentRoute.value);
|
||||
let currentUri = window.location.origin + "/#" + router.currentRoute.value.fullPath;
|
||||
let redirectUrl = `${WGPORTAL_BACKEND_BASE_URL}${provider.ProviderUrl}`;
|
||||
redirectUrl += "?redirect=true";
|
||||
redirectUrl += "&return=" + encodeURIComponent(currentUri);
|
||||
window.location.href = redirectUrl;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-lg-3"></div><!-- left spacer -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">{{ $t('login.headline') }}<div class="float-end">
|
||||
<RouterLink :to="{ name: 'home' }" class="nav-link" :title="$t('menu.home')"><i class="fas fa-times-circle"></i></RouterLink>
|
||||
</div></div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="inputUsername">{{ $t('login.username.label') }}</label>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><span class="far fa-user p-2"></span></span>
|
||||
<input id="inputUsername" v-model="username" :class="{'is-invalid':usernameInvalid, 'is-valid':!usernameInvalid}" :placeholder="$t('login.username.placeholder')" aria-describedby="usernameHelp"
|
||||
class="form-control"
|
||||
name="username" type="text">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="inputPassword">{{ $t('login.password.label') }}</label>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><span class="fas fa-lock p-2"></span></span>
|
||||
<input id="inputPassword" v-model="password" :class="{'is-invalid':passwordInvalid, 'is-valid':!passwordInvalid}" :placeholder="$t('login.password.placeholder')" class="form-control"
|
||||
name="password" type="password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-5 d-flex">
|
||||
<div :class="{'col-lg-4':auth.LoginProviders.length < 3, 'col-lg-12':auth.LoginProviders.length >= 3}" class="d-flex mb-2">
|
||||
<button :disabled="disableLoginBtn" class="btn btn-primary flex-fill" type="submit" @click.prevent="login">
|
||||
{{ $t('login.button') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||
</button>
|
||||
</div>
|
||||
<div :class="{'col-lg-8':auth.LoginProviders.length < 3, 'col-lg-12':auth.LoginProviders.length >= 3}" class="d-flex mb-2">
|
||||
<!-- OpenIdConnect / OAUTH providers -->
|
||||
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
||||
:disabled="loggingIn" :title="provider.Name" class="btn btn-outline-primary flex-fill"
|
||||
v-html="provider.Name" @click.prevent="externalLogin(provider)"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3"></div><!-- right spacer -->
|
||||
</div>
|
||||
</template>
|
126
frontend/src/views/ProfileView.vue
Normal file
126
frontend/src/views/ProfileView.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup>
|
||||
import PeerViewModal from "../components/PeerViewModal.vue";
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import {profileStore} from "@/stores/profile";
|
||||
import PeerEditModal from "@/components/PeerEditModal.vue";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
|
||||
const settings = settingsStore()
|
||||
const profile = profileStore()
|
||||
|
||||
const viewedPeerId = ref("")
|
||||
const editPeerId = ref("")
|
||||
|
||||
onMounted(async () => {
|
||||
await profile.LoadUser()
|
||||
await profile.LoadPeers()
|
||||
await profile.LoadStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId!==''" @close="viewedPeerId=''"></PeerViewModal>
|
||||
<PeerEditModal :peerId="editPeerId" :visible="editPeerId!==''" @close="editPeerId=''"></PeerEditModal>
|
||||
|
||||
<!-- Peer list -->
|
||||
<div class="mt-4 row">
|
||||
<div class="col-12 col-lg-5">
|
||||
<h2 class="mt-2">{{ $t('profile.headline') }}</h2>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 text-lg-end">
|
||||
<div class="form-group d-inline">
|
||||
<div class="input-group mb-3">
|
||||
<input v-model="profile.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="profile.afterPageSizeChange">
|
||||
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 text-lg-end">
|
||||
<a v-if="settings.Setting('SelfProvisioning')" class="btn btn-primary ms-2" href="#" :title="$t('general.search.button-add-peer')" @click.prevent="editPeerId='#NEW#'"><i class="fa fa-plus me-1"></i><i class="fa fa-user"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<div v-if="profile.CountPeers===0">
|
||||
<h4>{{ $t('profile.no-peer.headline') }}</h4>
|
||||
<p>{{ $t('profile.no-peer.abstract') }}</p>
|
||||
</div>
|
||||
<table v-if="profile.CountPeers!==0" id="peerTable" class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<input id="flexCheckDefault" class="form-check-input" :title="$t('general.select-all')" type="checkbox" value="">
|
||||
</th><!-- select -->
|
||||
<th scope="col"></th><!-- status -->
|
||||
<th scope="col">{{ $t('profile.table-heading.name') }}</th>
|
||||
<th scope="col">{{ $t('profile.table-heading.ip') }}</th>
|
||||
<th v-if="profile.hasStatistics" scope="col">{{ $t('profile.table-heading.stats') }}</th>
|
||||
<th scope="col">{{ $t('profile.table-heading.interface') }}</th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="peer in profile.FilteredAndPagedPeers" :key="peer.Identifier">
|
||||
<th scope="row">
|
||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
||||
</th>
|
||||
<td class="text-center">
|
||||
<span v-if="peer.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="peer.DisabledReason"></i></span>
|
||||
<span v-if="!peer.Disabled && peer.ExpiresAt" class="text-warning"><i class="fas fa-hourglass-end" :title="peer.ExpiresAt"></i></span>
|
||||
</td>
|
||||
<td><span v-if="peer.DisplayName" :title="peer.Identifier">{{peer.DisplayName}}</span><span v-else :title="peer.Identifier">{{$filters.truncate(peer.Identifier, 10)}}</span></td>
|
||||
<td>
|
||||
<span v-for="ip in peer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span>
|
||||
</td>
|
||||
<td v-if="profile.hasStatistics">
|
||||
<div v-if="profile.Statistics(peer.Identifier).IsConnected">
|
||||
<span class="badge rounded-pill bg-success"><i class="fa-solid fa-link"></i></span> <span :title="peers.Statistics(peer.Identifier).LastHandshake">{{ $t('profile.peer-connected') }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="badge rounded-pill bg-light"><i class="fa-solid fa-link-slash"></i></span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{peer.InterfaceIdentifier}}</td>
|
||||
<td class="text-center">
|
||||
<a href="#" :title="$t('profile.button-show-peer')" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
||||
<a href="#" :title="$t('profile.button-edit-peer')" @click.prevent="editPeerId=peer.Identifier"><i class="fas fa-cog"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="mt-3">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<ul class="pagination pagination-sm">
|
||||
<li :class="{disabled:profile.pageOffset===0}" class="page-item">
|
||||
<a class="page-link" @click="profile.previousPage">«</a>
|
||||
</li>
|
||||
|
||||
<li v-for="page in profile.pages" :key="page" :class="{active:profile.currentPage===page}" class="page-item">
|
||||
<a class="page-link" @click="profile.gotoPage(page)">{{page}}</a>
|
||||
</li>
|
||||
|
||||
<li :class="{disabled:!profile.hasNextPage}" class="page-item">
|
||||
<a class="page-link" @click="profile.nextPage">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||
<div class="col-sm-6">
|
||||
<select v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
126
frontend/src/views/UserView.vue
Normal file
126
frontend/src/views/UserView.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup>
|
||||
import {userStore} from "@/stores/users";
|
||||
import {ref,onMounted} from "vue";
|
||||
import UserEditModal from "../components/UserEditModal.vue";
|
||||
import UserViewModal from "../components/UserViewModal.vue";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
|
||||
const settings = settingsStore()
|
||||
const users = userStore()
|
||||
|
||||
const editUserId = ref("")
|
||||
const viewedUserId = ref("")
|
||||
|
||||
onMounted(() => {
|
||||
users.LoadUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UserEditModal :userId="editUserId" :visible="editUserId!==''" @close="editUserId=''"></UserEditModal>
|
||||
<UserViewModal :userId="viewedUserId" :visible="viewedUserId!==''" @close="viewedUserId=''"></UserViewModal>
|
||||
|
||||
<!-- User list -->
|
||||
<div class="mt-4 row">
|
||||
<div class="col-12 col-lg-5">
|
||||
<h1>{{ $t('users.headline') }}</h1>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 text-lg-end">
|
||||
<div class="form-group d-inline">
|
||||
<div class="input-group mb-3">
|
||||
<input v-model="users.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="users.afterPageSizeChange">
|
||||
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-3 text-lg-end">
|
||||
<a class="btn btn-primary ms-2" href="#" :title="$t('users.button-add-user')" @click.prevent="editUserId='#NEW#'">
|
||||
<i class="fa fa-plus me-1"></i><i class="fa fa-user"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<div v-if="users.Count===0">
|
||||
<h4>{{ $t('users.no-user.headline') }}</h4>
|
||||
<p>{{ $t('users.no-user.abstract') }}</p>
|
||||
</div>
|
||||
<table v-if="users.Count!==0" id="userTable" class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<input id="flexCheckDefault" class="form-check-input" :title="$t('general.select-all')" type="checkbox" value="">
|
||||
</th><!-- select -->
|
||||
<th scope="col"></th><!-- status -->
|
||||
<th scope="col">{{ $t('users.table-heading.id') }}</th>
|
||||
<th scope="col">{{ $t('users.table-heading.email') }}</th>
|
||||
<th scope="col">{{ $t('users.table-heading.firstname') }}</th>
|
||||
<th scope="col">{{ $t('users.table-heading.lastname') }}</th>
|
||||
<th class="text-center" scope="col">{{ $t('users.table-heading.source') }}</th>
|
||||
<th class="text-center" scope="col">{{ $t('users.table-heading.peers') }}</th>
|
||||
<th class="text-center" scope="col">{{ $t('users.table-heading.admin') }}</th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users.FilteredAndPaged" :key="user.Identifier">
|
||||
<th scope="row">
|
||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
||||
</th>
|
||||
<td class="text-center">
|
||||
<span v-if="user.Disabled" class="text-danger" :title="$t('users.user-disabled') + ' ' + user.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
||||
<span v-if="user.Locked" class="text-danger" :title="$t('users.user-locked') + ' ' + user.LockedReason"><i class="fas fa-lock"></i></span>
|
||||
</td>
|
||||
<td>{{user.Identifier}}</td>
|
||||
<td>{{user.Email}}</td>
|
||||
<td>{{user.Firstname}}</td>
|
||||
<td>{{user.Lastname}}</td>
|
||||
<td class="text-center"><span class="badge rounded-pill bg-light">{{user.Source}}</span></td>
|
||||
<td class="text-center">{{user.PeerCount}}</td>
|
||||
<td class="text-center">
|
||||
<span v-if="user.IsAdmin" class="text-danger" :title="$t('users.admin')"><i class="fa fa-check-circle"></i></span>
|
||||
<span v-else><i class="fa fa-circle-xmark" :title="$t('users.no-admin')"></i></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="#" :title="$t('users.button-show-user')" @click.prevent="viewedUserId=user.Identifier"><i class="fas fa-eye me-2"></i></a>
|
||||
<a href="#" :title="$t('users.button-edit-user')" @click.prevent="editUserId=user.Identifier"><i class="fas fa-cog me-2"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="mt-3">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<ul class="pagination pagination-sm">
|
||||
<li :class="{disabled:users.pageOffset===0}" class="page-item">
|
||||
<a class="page-link" @click="users.previousPage">«</a>
|
||||
</li>
|
||||
|
||||
<li v-for="page in users.pages" :key="page" :class="{active:users.currentPage===page}" class="page-item">
|
||||
<a class="page-link" @click="users.gotoPage(page)">{{page}}</a>
|
||||
</li>
|
||||
|
||||
<li :class="{disabled:!users.hasNextPage}" class="page-item">
|
||||
<a class="page-link" @click="users.nextPage">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||
<div class="col-sm-6">
|
||||
<select v-model.number="users.pageSize" class="form-select" @click="users.afterPageSizeChange()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
34
frontend/vite.config.js
Normal file
34
frontend/vite.config.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { fileURLToPath, URL } from 'url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: '../internal/app/api/core/frontend-dist',
|
||||
emptyOutDir: true
|
||||
},
|
||||
// local dev api (proxy to avoid cors problems)
|
||||
server: {
|
||||
port: 5000,
|
||||
proxy: {
|
||||
"/api/v0": {
|
||||
target: "http://localhost:8888",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"x-wg-dev": true,
|
||||
},
|
||||
rewrite: (path) => path,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
Reference in New Issue
Block a user