Brought into working condition for LDAP authentication.

This commit is contained in:
sh0rch 2024-02-29 07:17:17 +03:00
parent 1b4b5ff161
commit 0ade556e80
12 changed files with 821 additions and 261 deletions

View File

@ -54,7 +54,7 @@ func generateApi(basePath, apiPath, version string) error {
OutputDir: filepath.Join(basePath, "core/assets/doc"),
OutputTypes: []string{"json", "yaml"},
ParseVendor: false,
ParseDependency: true,
ParseDependency: 3,
MarkdownFilesDir: "",
ParseInternal: true,
GeneratedTime: false,

View File

@ -1,9 +1,9 @@
<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";
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()
@ -80,10 +80,11 @@ const currentYear = ref(new Date().getFullYear())
<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>
<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>
<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') }}
@ -107,22 +108,24 @@ const currentYear = ref(new Date().getFullYear())
<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">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>
<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>
<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('ru')"><span class="fi fi-ru"></span>
Русский</a>
</div>
</div>
</div>
</div>
</div>
</div>
</footer>
</template>
</footer></template>
<style>
</style>
<style></style>

View File

@ -1,20 +1,22 @@
<script setup>
import Modal from "./Modal.vue";
import {peerStore} from "@/stores/peers";
import {interfaceStore} from "@/stores/interfaces";
import {computed, ref, watch} from "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 { isIP } from 'is-ip';
import { freshPeer, freshInterface } from '@/helpers/models';
import { profileStore } from "@/stores/profile";
const { t } = useI18n()
const peers = peerStore()
const interfaces = interfaceStore()
const profile = profileStore()
const props = defineProps({
peerId: String,
@ -24,7 +26,16 @@ const props = defineProps({
const emit = defineEmits(['close'])
const selectedPeer = computed(() => {
return peers.Find(props.peerId)
let p = peers.Find(props.peerId)
if (!p) {
if (!!props.peerId || props.peerId.length) {
p = profile.peers.find((p) => p.Identifier === props.peerId)
} else {
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
}
}
return p
})
const selectedInterface = computed(() => {
@ -59,121 +70,119 @@ 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)
if (oldValue === false && newValue === true) { // if modal is shown
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.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.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.PrivateKey = peers.Prepared.PrivateKey
formData.value.PublicKey = peers.Prepared.PublicKey
formData.value.Mode = peers.Prepared.Mode
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.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
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
} 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.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.PrivateKey = selectedPeer.value.PrivateKey
formData.value.PublicKey = selectedPeer.value.PublicKey
formData.value.Mode = selectedPeer.value.Mode
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.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
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
}
}
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
}
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
}
}
if (oldValue && !newValue && formData.value.ExpiresAt) {
formData.value.ExpiresAt = "" // reset expiry date
}
}
)
function close() {
@ -184,7 +193,7 @@ function close() {
function handleChangeAddresses(tags) {
let validInput = true
tags.forEach(tag => {
if(isCidr(tag) === 0) {
if (isCidr(tag) === 0) {
validInput = false
notify({
title: "Invalid CIDR",
@ -193,7 +202,7 @@ function handleChangeAddresses(tags) {
})
}
})
if(validInput) {
if (validInput) {
formData.value.Addresses = tags
}
}
@ -201,7 +210,7 @@ function handleChangeAddresses(tags) {
function handleChangeAllowedIPs(tags) {
let validInput = true
tags.forEach(tag => {
if(isCidr(tag) === 0) {
if (isCidr(tag) === 0) {
validInput = false
notify({
title: "Invalid CIDR",
@ -210,7 +219,7 @@ function handleChangeAllowedIPs(tags) {
})
}
})
if(validInput) {
if (validInput) {
formData.value.AllowedIPs.Value = tags
}
}
@ -218,7 +227,7 @@ function handleChangeAllowedIPs(tags) {
function handleChangeExtraAllowedIPs(tags) {
let validInput = true
tags.forEach(tag => {
if(isCidr(tag) === 0) {
if (isCidr(tag) === 0) {
validInput = false
notify({
title: "Invalid CIDR",
@ -227,7 +236,7 @@ function handleChangeExtraAllowedIPs(tags) {
})
}
})
if(validInput) {
if (validInput) {
formData.value.ExtraAllowedIPs = tags
}
}
@ -235,7 +244,7 @@ function handleChangeExtraAllowedIPs(tags) {
function handleChangeDns(tags) {
let validInput = true
tags.forEach(tag => {
if(!isIP(tag)) {
if (!isIP(tag)) {
validInput = false
notify({
title: "Invalid IP",
@ -244,7 +253,7 @@ function handleChangeDns(tags) {
})
}
})
if(validInput) {
if (validInput) {
formData.value.Dns.Value = tags
}
}
@ -255,14 +264,14 @@ function handleChangeDnsSearch(tags) {
async function save() {
try {
if (props.peerId!=='#NEW#') {
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)
// console.log(e)
notify({
title: "Failed to save peer!",
text: e.toString(),
@ -276,7 +285,7 @@ async function del() {
await peers.DeletePeer(selectedPeer.value.Identifier)
close()
} catch (e) {
console.log(e)
// console.log(e)
notify({
title: "Failed to delete peer!",
text: e.toString(),
@ -294,87 +303,86 @@ async function del() {
<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">
<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">
<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'">
<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">
<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">
<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">
<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'">
<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">
<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'">
<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">
<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"/>
: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"/>
: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"/>
: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"/>
: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">
<div hidden 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"/>
: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">
<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">
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.mtu.label')"
v-model="formData.Mtu.Value">
</div>
</div>
</fieldset>
@ -382,19 +390,23 @@ async function del() {
<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>
<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>
<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>
<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>
<textarea v-model="formData.PostDown.Value" class="form-control" rows="2"
:placeholder="$t('modals.peer-edit.post-down.placeholder')"></textarea>
</div>
</fieldset>
<fieldset>
@ -403,7 +415,7 @@ async function del() {
<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>
<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">
@ -412,14 +424,16 @@ async function del() {
</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">
<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>
<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>
@ -427,5 +441,4 @@ async function del() {
</Modal>
</template>
<style>
</style>
<style></style>

View File

@ -1,19 +1,23 @@
<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 { 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";
import { notify } from "@kyvg/vue3-notification";
import { settingsStore } from "@/stores/settings";
import { profileStore } from "@/stores/profile";
import { base64_url_encode } from '@/helpers/encoding';
import { apiWrapper } from "@/helpers/fetch-wrapper";
const { t } = useI18n()
const settings = settingsStore()
const peers = peerStore()
const interfaces = interfaceStore()
const profile = profileStore()
const props = defineProps({
peerId: String,
@ -32,9 +36,12 @@ const selectedPeer = computed(() => {
let p = peers.Find(props.peerId)
if (!p) {
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
if (!!props.peerId || props.peerId.length) {
p = profile.peers.find((p) => p.Identifier === props.peerId)
} else {
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
}
}
return p
})
@ -42,9 +49,13 @@ const selectedStats = computed(() => {
let s = peers.Statistics(props.peerId)
if (!s) {
s = freshStats() // dummy peer to avoid 'undefined' exceptions
}
if (!!props.peerId || props.peerId.length) {
p = profile.Statistics(props.peerId)
} else {
s = freshStats() // dummy peer to avoid 'undefined' exceptions
}
}
return s
})
@ -54,7 +65,6 @@ const selectedInterface = computed(() => {
if (!i) {
i = freshInterface() // dummy interface to avoid 'undefined' exceptions
}
return i
})
@ -70,11 +80,11 @@ const title = computed(() => {
})
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
}
}
if (oldValue === false && newValue === true) { // if modal is shown
await peers.LoadPeerConfig(selectedPeer.value.Identifier)
configString.value = peers.configuration
}
}
)
function download() {
@ -82,10 +92,10 @@ function 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"
.replace(/ /g, "_")
.replace(/[^a-zA-Z0-9-_]/g, "")
.substring(0, 16)
+ ".conf"
}
let text = configString.value
@ -110,6 +120,13 @@ function email() {
})
}
function ConfigQrUrl() {
if (props.peerId.length) {
return apiWrapper.url(`/peer/config-qr/${base64_url_encode(props.peerId)}`)
}
return ''
}
</script>
<template>
@ -118,25 +135,30 @@ function email() {
<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">
<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 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.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>
<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">
<img class="config-qr-img" :src="ConfigQrUrl()" loading="lazy" alt="Configuration QR Code">
</div>
</div>
</div>
@ -144,16 +166,20 @@ function email() {
</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">
<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 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>
<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>
@ -166,13 +192,15 @@ function email() {
</div>
</div>
</div>
<div v-if="selectedInterface.Mode==='server'" class="accordion-item">
<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">
<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 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>
@ -182,18 +210,17 @@ function email() {
</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>
<button @click.prevent="download" type="button" class="btn btn-primary me-1">{{
$t('modals.peer-view.button-download') }}</button>
<button @click.prevent="email" hidden 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>
</template>
</Modal></template>
<style>
.config-qr-img {
<style>.config-qr-img {
max-width: 100%;
}
</style>
}</style>

View File

@ -1,5 +1,6 @@
// src/lang/index.js
import de from './translations/de.json';
import ru from './translations/ru.json';
import en from './translations/en.json';
import {createI18n} from "vue-i18n";
@ -20,6 +21,7 @@ const i18n = createI18n({
fallbackLocale: "en", // set fallback locale
messages: {
"de": de,
"ru": ru,
"en": en
}
});

View File

@ -45,7 +45,8 @@
"box-header": "WireGuard Installation",
"headline": "Installation",
"content": "Installation instructions for client software can be found on the official WireGuard website.",
"btn": "Open Instructions"
"btn": "Open Instructions",
"button": "Open Instructions"
},
"about-wg": {
"box-header": "About WireGuard",

View File

@ -0,0 +1,489 @@
{
"general": {
"pagination": {
"size": "Количество элементов",
"all": "Все (медленно)"
},
"search": {
"placeholder": "Поиск...",
"button": "Поиск"
},
"select-all": "Выбрать все",
"yes": "Да",
"no": "Нет",
"cancel": "Отмена",
"close": "Закрыть",
"save": "Сохранить",
"delete": "Удалить"
},
"login": {
"headline": "Пожалуйста, войдите в систему",
"username": {
"label": "Имя пользователя",
"placeholder": "Пожалуйста, введите ваше имя пользователя"
},
"password": {
"label": "Пароль",
"placeholder": "Пожалуйста, введите ваш пароль"
},
"button": "Войти"
},
"menu": {
"home": "Главная",
"interfaces": "Интерфейсы",
"users": "Пользователи",
"lang": "Сменить язык",
"profile": "Мой профиль",
"login": "Вход",
"logout": "Выход"
},
"home": {
"headline": "Портал VPN WireGuard®",
"info-headline": "Дополнительная информация",
"abstract": "WireGuard® - это чрезвычайно простой, но быстрый и современный VPN, использующий передовую криптографию. Он стремится быть быстрее, проще, компактнее и полезнее, чем IPsec, избегая при этом значительных сложностей. Он предназначен для значительного повышения производительности по сравнению с OpenVPN.",
"installation": {
"box-header": "Установка WireGuard",
"headline": "Установка",
"content": "Инструкции по установке клиентского программного обеспечения можно найти на официальном сайте WireGuard.",
"btn": "Открыть инструкции",
"button": "Открыть инструкции"
},
"about-wg": {
"box-header": "О WireGuard",
"headline": "О программе",
"content": "WireGuard® - это чрезвычайно простой, но быстрый и современный VPN, использующий передовую криптографию.",
"button": "Подробнее"
},
"about-portal": {
"box-header": "О портале WireGuard",
"headline": "Портал WireGuard",
"content": "Портал WireGuard - это простой веб-портал для настройки WireGuard.",
"button": "Подробнее"
},
"profiles": {
"headline": "VPN Профили",
"abstract": "Вы можете получить доступ и загрузить свои личные конфигурации VPN через свой пользовательский профиль.",
"content": "Чтобы найти все сконфигурированные профили, нажмите на кнопку ниже.",
"button": "Открыть мой профиль"
},
"admin": {
"headline": "Административная зона",
"abstract": "В административной зоне вы можете управлять узлами и серверным интерфейсом WireGuard, а также пользователями, которым разрешен вход в портал WireGuard.",
"content": "",
"button-admin": "Открыть администрирование сервера",
"button-user": "Открыть администрирование пользователей"
}
},
"interfaces": {
"headline": "Администрирование интерфейсов",
"headline-peers": "Текущие VPN пиры",
"headline-endpoints": "Текущие конечные точки",
"no-interface": {
"default-selection": "Интерфейсы отсутствуют",
"headline": "Интерфейсы не найдены...",
"abstract": "Нажмите на кнопку со знаком плюса выше, чтобы создать новый интерфейс WireGuard."
},
"no-peer": {
"headline": "Пиры отсутствуют",
"abstract": "В настоящее время для выбранного интерфейса WireGuard нет доступных пиров."
},
"table-heading": {
"name": "Имя",
"user": "Пользователь",
"ip": "IP-адреса",
"endpoint": "Конечная точка",
"status": "Статус"
},
"interface": {
"headline": "Статус интерфейса для",
"mode": "режим",
"key": "Публичный ключ",
"endpoint": "Публичная конечная точка",
"port": "Порт прослушивания",
"peers": "Активные пиры",
"total-peers": "Всего пиров",
"endpoints": "Активные конечные точки",
"total-endpoints": "Всего конечных точек",
"ip": "IP-адрес",
"default-allowed-ip": "Разрешенные IP по умолчанию",
"dns": "DNS-серверы",
"mtu": "MTU",
"default-keep-alive": "Интервал поддержания активности по умолчанию",
"button-show-config": "Показать конфигурацию",
"button-download-config": "Скачать конфигурацию",
"button-store-config": "Сохранить конфигурацию для wg-quick",
"button-edit": "Редактировать интерфейс"
},
"button-add-interface": "Добавить интерфейс",
"button-add-peer": "Добавить пира",
"button-add-peers": "Добавить несколько пиров",
"button-show-peer": "Показать пира",
"button-edit-peer": "Редактировать пира",
"peer-disabled": "Пир отключен, причина:",
"peer-expiring": "Пир истекает в",
"peer-connected": "Подключено",
"peer-not-connected": "Не подключено",
"peer-handshake": "Последнее рукопожатие:"
},
"users": {
"headline": "Администрирование пользователей",
"table-heading": {
"id": "ID",
"email": "Электронная почта",
"firstname": "Имя",
"lastname": "Фамилия",
"source": "Источник",
"peers": "Пиры",
"admin": "Админ"
},
"no-user": {
"headline": "Пользователи отсутствуют",
"abstract": "В настоящее время в портале WireGuard не зарегистрировано ни одного пользователя."
},
"button-add-user": "Добавить пользователя",
"button-show-user": "Показать пользователя",
"button-edit-user": "Редактировать пользователя",
"user-disabled": "Пользователь отключен, причина:",
"user-locked": "Учетная запись заблокирована, причина:",
"admin": "Пользователь имеет права администратора",
"no-admin": "Пользователь не имеет прав администратора"
},
"profile": {
"headline": "Мои VPN пиры",
"table-heading": {
"name": "Имя",
"ip": "IP-адреса",
"stats": "Статус",
"interface": "Интерфейс сервера"
},
"no-peer": {
"headline": "Пиров нет",
"abstract": "В настоящее время у вашего профиля пользователя нет связанных пиров."
},
"peer-connected": "Подключено",
"button-add-peer": "Добавить пира",
"button-show-peer": "Показать пира",
"button-edit-peer": "Редактировать пира"
},
"modals": {
"user-view": {
"headline": "Учетная запись пользователя:",
"tab-user": "Информация",
"tab-peers": "Пиры",
"headline-info": "Информация о пользователе:",
"headline-notes": "Заметки:",
"email": "Электронная почта",
"firstname": "Имя",
"lastname": "Фамилия",
"phone": "Номер телефона",
"department": "Отдел",
"disabled": "Учетная запись отключена",
"locked": "Учетная запись заблокирована",
"no-peers": "У пользователя нет связанных пиров.",
"peers": {
"name": "Имя",
"interface": "Интерфейс",
"ip": "IP-адреса"
}
},
"user-edit": {
"headline-edit": "Редактировать пользователя:",
"headline-new": "Новый пользователь",
"header-general": "Общее",
"header-personal": "Информация о пользователе",
"header-notes": "Заметки",
"header-state": "Состояние",
"identifier": {
"label": "Идентификатор",
"placeholder": "Уникальный идентификатор пользователя"
},
"source": {
"label": "Источник",
"placeholder": "Источник пользователя"
},
"password": {
"label": "Пароль",
"placeholder": "Надежный пароль",
"description": "Оставьте это поле пустым, чтобы сохранить текущий пароль."
},
"email": {
"label": "Электронная почта",
"placeholder": "Адрес электронной почты"
},
"phone": {
"label": "Телефон",
"placeholder": "Номер телефона"
},
"department": {
"label": "Отдел",
"placeholder": "Отдел"
},
"firstname": {
"label": "Имя",
"placeholder": "Имя"
},
"lastname": {
"label": "Фамилия",
"placeholder": "Фамилия"
},
"notes": {
"label": "Заметки",
"placeholder": ""
},
"disabled": {
"label": "Отключен (нет возможности подключения к WireGuard и входа в систему)"
},
"locked": {
"label": "Заблокирован (вход в систему невозможен, подключения WireGuard работают)"
},
"admin": {
"label": "Является администратором"
}
},
"interface-view": {
"headline": "Конфигурация интерфейса:"
},
"interface-edit": {
"headline-edit": "Редактировать интерфейс:",
"headline-new": "Новый интерфейс",
"tab-interface": "Интерфейс",
"tab-peerdef": "Настройки пира по умолчанию",
"header-general": "Общие",
"header-network": "Сеть",
"header-crypto": "Криптография",
"header-hooks": "Хуки интерфейса",
"header-peer-hooks": "Хуки",
"header-state": "Состояние",
"identifier": {
"label": "Идентификатор",
"placeholder": "Уникальный идентификатор интерфейса"
},
"mode": {
"label": "Режим интерфейса",
"server": "Режим сервера",
"client": "Режим клиента",
"any": "Неизвестный режим"
},
"display-name": {
"label": "Отображаемое имя",
"placeholder": "Описательное имя для интерфейса"
},
"private-key": {
"label": "Приватный ключ",
"placeholder": "Приватный ключ"
},
"public-key": {
"label": "Публичный ключ",
"placeholder": "Публичный ключ"
},
"ip": {
"label": "IP-адреса",
"placeholder": "IP-адреса (в формате CIDR)"
},
"listen-port": {
"label": "Порт прослушивания",
"placeholder": "Порт для прослушивания"
},
"dns": {
"label": "DNS-сервер",
"placeholder": "Используемые DNS-серверы"
},
"dns-search": {
"label": "Поисковые домены DNS",
"placeholder": "Префиксы поиска DNS"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU интерфейса (0 = использовать значение по умолчанию)"
},
"firewall-mark": {
"label": "Метка брандмауэра",
"placeholder": "Метка брандмауэра, применяемая к исходящему трафику (0 = автоматически)"
},
"routing-table": {
"label": "Таблица маршрутизации",
"placeholder": "ID таблицы маршрутизации",
"description": "Особые случаи: off = не управлять маршрутами, 0 = автоматически"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Одна или несколько команд bash, разделенных ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Одна или несколько команд bash, разделенных ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Одна или несколько команд bash, разделенных ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Одна или несколько команд bash, разделенных ;"
},
"disabled": {
"label": "Интерфейс отключен"
},
"save-config": {
"label": "Автоматически сохранять конфигурацию wg-quick"
},
"defaults": {
"endpoint": {
"label": "Адрес конечной точки",
"placeholder": "Адрес конечной точки",
"description": "Адрес конечной точки, к которой будут подключаться пиры."
},
"networks": {
"label": "IP-сети",
"placeholder": "Сетевые адреса",
"description": "Пиры будут получать IP-адреса из этих подсетей."
},
"allowed-ip": {
"label": "Разрешенные IP-адреса",
"placeholder": "Разрешенные IP-адреса по умолчанию"
},
"mtu": {
"label": "MTU",
"placeholder": "MTU клиента (0 = использовать значение по умолчанию)"
},
"keep-alive": {
"label": "Интервал поддержания активности",
"placeholder": "Постоянное поддержание активности (0 = значение по умолчанию)"
}
},
"button-apply-defaults": "Применить настройки пира по умолчанию"
},
"peer-view": {
"headline-peer": "Пир:",
"headline-endpoint": "Конечная точка:",
"section-info": "Информация о пире",
"section-status": "Текущий статус",
"section-config": "Конфигурация",
"identifier": "Идентификатор",
"ip": "IP-адреса",
"user": "Связанный пользователь",
"notes": "Заметки",
"expiry-status": "Истекает в",
"disabled-status": "Отключено в",
"traffic": "Трафик",
"connection-status": "Статус соединения",
"upload": "Загружено байт (от сервера к пиру)",
"download": "Скачано байт (от пира к серверу)",
"pingable": "Доступность пинга",
"handshake": "Последнее рукопожатие",
"connected-since": "Подключен с",
"endpoint": "Конечная точка",
"button-download": "Скачать конфигурацию",
"button-email": "Отправить конфигурацию по электронной почте"
},
"peer-edit": {
"headline-edit-peer": "Редактировать пира:",
"headline-edit-endpoint": "Редактировать конечную точку:",
"headline-new-peer": "Создать пира",
"headline-new-endpoint": "Создать конечную точку",
"header-general": "Общее",
"header-network": "Сеть",
"header-crypto": "Криптография",
"header-hooks": "Хуки (Выполняются на пире)",
"header-state": "Состояние",
"display-name": {
"label": "Отображаемое имя",
"placeholder": "Описательное имя для пира"
},
"linked-user": {
"label": "Связанный пользователь",
"placeholder": "Учетная запись пользователя, которой принадлежит этот пир"
},
"private-key": {
"label": "Приватный ключ",
"placeholder": "Приватный ключ"
},
"public-key": {
"label": "Публичный ключ",
"placeholder": "Публичный ключ"
},
"preshared-key": {
"label": "Предварительно разделяемый ключ",
"placeholder": "Необязательный предварительно разделяемый ключ"
},
"endpoint-public-key": {
"label": "Публичный ключ конечной точки",
"placeholder": "Публичный ключ удаленной конечной точки"
},
"endpoint": {
"label": "Адрес конечной точки",
"placeholder": "Адрес удаленной конечной точки"
},
"ip": {
"label": "IP-адреса",
"placeholder": "IP-адреса (в формате CIDR)"
},
"allowed-ip": {
"label": "Разрешенные IP-адреса",
"placeholder": "Разрешенные IP-адреса (в формате CIDR)"
},
"extra-allowed-ip": {
"label": "Дополнительно разрешенные IP-адреса",
"placeholder": "Дополнительные разрешенные IP-адреса (на стороне сервера)",
"description": "Эти IP-адреса будут добавлены в удаленный интерфейс WireGuard как разрешенные IP-адреса."
},
"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."
}
}
}
}

View File

@ -1,8 +1,8 @@
<script setup>
import {authStore} from "@/stores/auth";
import {RouterLink} from "vue-router";
import { authStore } from "@/stores/auth";
import { RouterLink } from "vue-router";
const auth = authStore()
const auth = authStore()
</script>
<template>
@ -29,7 +29,8 @@
<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: '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>

View File

@ -1,10 +1,10 @@
<script setup>
import PeerViewModal from "../components/PeerViewModal.vue";
import {onMounted, ref} from "vue";
import {profileStore} from "@/stores/profile";
import { onMounted, ref } from "vue";
import { profileStore } from "@/stores/profile";
import PeerEditModal from "@/components/PeerEditModal.vue";
import {settingsStore} from "@/stores/settings";
import { settingsStore } from "@/stores/settings";
const settings = settingsStore()
const profile = profileStore()
@ -17,11 +17,12 @@ onMounted(async () => {
await profile.LoadPeers()
await profile.LoadStats()
})
</script>
<template>
<PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId!==''" @close="viewedPeerId=''"></PeerViewModal>
<PeerEditModal :peerId="editPeerId" :visible="editPeerId!==''" @close="editPeerId=''"></PeerEditModal>
<PeerViewModal :peerId="viewedPeerId" :visible="viewedPeerId !== ''" @close="viewedPeerId = ''"></PeerViewModal>
<PeerEditModal :peerId="editPeerId" :visible="editPeerId !== ''" @close="editPeerId = ''"></PeerEditModal>
<!-- Peer list -->
<div class="mt-4 row">
@ -31,33 +32,38 @@ onMounted(async () => {
<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>
<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>
<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 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">
<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>
<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">
@ -65,25 +71,31 @@ onMounted(async () => {
<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>
<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-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>
<span class="badge rounded-pill bg-success"><i class="fa-solid fa-link"></i></span>
<span :title="profile.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>{{ 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>
<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>
@ -94,33 +106,34 @@ onMounted(async () => {
<div class="row">
<div class="col-6">
<ul class="pagination pagination-sm">
<li :class="{disabled:profile.pageOffset===0}" class="page-item">
<li :class="{ disabled: profile.pageOffset === 0 }" class="page-item">
<a class="page-link" @click="profile.previousPage">&laquo;</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 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">
<li :class="{ disabled: !profile.hasNextPage }" class="page-item">
<a class="page-link" @click="profile.nextPage">&raquo;</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>
<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>
<option value="100">100</option>
<option value="999999999">{{ $t('general.pagination.all') }}</option>
</select>
</div>
</div>
</div>
</div>
</template>
</div></template>

View File

@ -4,11 +4,12 @@ import (
"context"
"errors"
"fmt"
"time"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/sirupsen/logrus"
evbus "github.com/vardius/message-bus"
"time"
)
type App struct {
@ -59,6 +60,7 @@ func New(cfg *config.Config, bus evbus.MessageBus, authenticator Authenticator,
}
func (a *App) Startup(ctx context.Context) error {
a.UserManager.StartBackgroundJobs(ctx)
a.StatisticsCollector.StartBackgroundJobs(ctx)
a.WireGuardManager.StartBackgroundJobs(ctx)

View File

@ -4,11 +4,12 @@ import (
"context"
"errors"
"fmt"
"github.com/h44z/wg-portal/internal/app"
"math"
"sync"
"time"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal"
"github.com/go-ldap/ldap/v3"
@ -87,7 +88,9 @@ func (m Manager) NewUser(ctx context.Context, user *domain.User) error {
}
func (m Manager) StartBackgroundJobs(ctx context.Context) {
go m.runLdapSynchronizationService(ctx)
}
func (m Manager) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
@ -322,7 +325,7 @@ func (m Manager) runLdapSynchronizationService(ctx context.Context) {
if !ldapCfg.Synchronize {
continue // sync disabled
}
//logrus.Tracef(&ldapCfg)
err := m.synchronizeLdapUsers(ctx, &ldapCfg)
if err != nil {
logrus.Errorf("failed to synchronize LDAP users for %s: %v", ldapCfg.ProviderName, err)
@ -382,15 +385,20 @@ func (m Manager) updateLdapUsers(ctx context.Context, providerName string, rawUs
return fmt.Errorf("find error for user id %s: %w", user.Identifier, err)
}
tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
tctx = domain.SetUserInfo(tctx, domain.SystemAdminContextUserInfo())
if existingUser == nil {
err := m.NewUser(ctx, user)
err := m.NewUser(tctx, user)
if err != nil {
return fmt.Errorf("create error for user id %s: %w", user.Identifier, err)
}
}
if existingUser != nil && existingUser.Source == domain.UserSourceLdap && userChangedInLdap(existingUser, user) {
err := m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
err := m.users.SaveUser(tctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
u.UpdatedAt = time.Now()
u.UpdatedBy = "ldap_sync"
u.Email = user.Email

View File

@ -3,9 +3,10 @@ package internal
import (
"crypto/tls"
"fmt"
"github.com/sirupsen/logrus"
"os"
"github.com/sirupsen/logrus"
"github.com/go-ldap/ldap/v3"
"github.com/h44z/wg-portal/internal/config"
)