mirror of
https://github.com/h44z/wg-portal.git
synced 2025-09-15 15:21:14 +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:
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>
|
Reference in New Issue
Block a user