mirror of
https://github.com/vx3r/wg-gen-web.git
synced 2024-11-04 17:31:17 +00:00
Integrate https://github.com/jamescun/wg-api stats into the UI
This commit is contained in:
parent
8b4038c238
commit
31dd494d62
5
.env
5
.env
@ -39,3 +39,8 @@ OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr
|
||||
|
||||
# set provider name to fake to disable auth, also the default
|
||||
OAUTH2_PROVIDER_NAME=fake
|
||||
|
||||
# https://github.com/jamescun/wg-api integration, user and password (basic auth) are optional
|
||||
WG_STATS_API=https://wg.example.digital/wg-api
|
||||
WG_STATS_API_USER=
|
||||
WG_STATS_API_PASS=
|
44
api/v1/status/status.go
Normal file
44
api/v1/status/status.go
Normal file
@ -0,0 +1,44 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
|
||||
)
|
||||
|
||||
// ApplyRoutes applies router to gin Router
|
||||
func ApplyRoutes(r *gin.RouterGroup) {
|
||||
g := r.Group("/status")
|
||||
{
|
||||
g.GET("/interface", readInterfaceStatus)
|
||||
g.GET("/clients", readClientStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func readInterfaceStatus(c *gin.Context) {
|
||||
status, err := core.ReadInterfaceStatus()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read interface status")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
func readClientStatus(c *gin.Context) {
|
||||
status, err := core.ReadClientStatus()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read client status")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, status)
|
||||
}
|
@ -5,6 +5,7 @@ import (
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/auth"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/client"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/server"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/status"
|
||||
)
|
||||
|
||||
// ApplyRoutes apply routes to gin router
|
||||
@ -14,9 +15,9 @@ func ApplyRoutes(r *gin.RouterGroup, private bool) {
|
||||
if private {
|
||||
client.ApplyRoutes(v1)
|
||||
server.ApplyRoutes(v1)
|
||||
status.ApplyRoutes(v1)
|
||||
} else {
|
||||
auth.ApplyRoutes(v1)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
168
core/status.go
Normal file
168
core/status.go
Normal file
@ -0,0 +1,168 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||
)
|
||||
|
||||
// apiError implements a top-level JSON-RPC error.
|
||||
type apiError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type apiRequest struct {
|
||||
Version string `json:"jsonrpc"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
Version string `json:"jsonrpc"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
Error *apiError `json:"error,omitempty"`
|
||||
ID json.RawMessage `json:"id"`
|
||||
}
|
||||
|
||||
func fetchWireGuardAPI(reqData apiRequest) (*apiResponse, error) {
|
||||
apiUrl := os.Getenv("WG_STATS_API")
|
||||
apiClient := http.Client{
|
||||
Timeout: time.Second * 2, // Timeout after 2 seconds
|
||||
}
|
||||
jsonData, _ := json.Marshal(reqData)
|
||||
req, err := http.NewRequest(http.MethodPost, apiUrl, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "wg-gen-web")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Cache-Control", "no-cache")
|
||||
|
||||
if os.Getenv("WG_STATS_API_USER") != "" {
|
||||
req.SetBasicAuth(os.Getenv("WG_STATS_API_USER"), os.Getenv("WG_STATS_API_PASS"))
|
||||
}
|
||||
|
||||
res, getErr := apiClient.Do(req)
|
||||
if getErr != nil {
|
||||
return nil, getErr
|
||||
}
|
||||
|
||||
if res.Body != nil {
|
||||
defer res.Body.Close()
|
||||
}
|
||||
|
||||
body, readErr := ioutil.ReadAll(res.Body)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
|
||||
response := apiResponse{}
|
||||
jsonErr := json.Unmarshal(body, &response)
|
||||
if jsonErr != nil {
|
||||
return nil, jsonErr
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// ReadInterfaceStatus object, create default one
|
||||
func ReadInterfaceStatus() (*model.InterfaceStatus, error) {
|
||||
interfaceStatus := &model.InterfaceStatus{
|
||||
Name: "unknown",
|
||||
DeviceType: "unknown",
|
||||
ListenPort: 0,
|
||||
NumberOfPeers: 0,
|
||||
PublicKey: "",
|
||||
}
|
||||
|
||||
data, err := fetchWireGuardAPI(apiRequest{
|
||||
Version: "2.0",
|
||||
Method: "GetDeviceInfo",
|
||||
Params: nil,
|
||||
})
|
||||
if err != nil {
|
||||
return interfaceStatus, err
|
||||
}
|
||||
|
||||
resultData := data.Result.(map[string]interface{})
|
||||
device := resultData["device"].(map[string]interface{})
|
||||
interfaceStatus.Name = device["name"].(string)
|
||||
interfaceStatus.DeviceType = device["type"].(string)
|
||||
interfaceStatus.PublicKey = device["public_key"].(string)
|
||||
interfaceStatus.ListenPort = int(device["listen_port"].(float64))
|
||||
interfaceStatus.NumberOfPeers = int(device["num_peers"].(float64))
|
||||
|
||||
return interfaceStatus, nil
|
||||
}
|
||||
|
||||
// ReadClientStatus object, create default one, last recent active client is listed first
|
||||
func ReadClientStatus() ([]*model.ClientStatus, error) {
|
||||
var clientStatus []*model.ClientStatus
|
||||
|
||||
data, err := fetchWireGuardAPI(apiRequest{
|
||||
Version: "2.0",
|
||||
Method: "ListPeers",
|
||||
Params: []byte("{}"),
|
||||
})
|
||||
if err != nil {
|
||||
return clientStatus, err
|
||||
}
|
||||
|
||||
resultData := data.Result.(map[string]interface{})
|
||||
peers := resultData["peers"].([]interface{})
|
||||
|
||||
clients, err := ReadClients()
|
||||
withClientDetails := true
|
||||
if err != nil {
|
||||
withClientDetails = false
|
||||
}
|
||||
|
||||
for _, tmpPeer := range peers {
|
||||
peer := tmpPeer.(map[string]interface{})
|
||||
peerHandshake, _ := time.Parse(time.RFC3339Nano, peer["last_handshake"].(string))
|
||||
peerIPs := peer["allowed_ips"].([]interface{})
|
||||
peerAddresses := make([]string, len(peerIPs))
|
||||
for i, peerIP := range peerIPs {
|
||||
peerAddresses[i] = peerIP.(string)
|
||||
}
|
||||
|
||||
newClientStatus := &model.ClientStatus{
|
||||
PublicKey: peer["public_key"].(string),
|
||||
HasPresharedKey: peer["has_preshared_key"].(bool),
|
||||
ProtocolVersion: int(peer["protocol_version"].(float64)),
|
||||
Name: "UNKNOWN",
|
||||
Email: "UNKNOWN",
|
||||
Connected: false,
|
||||
AllowedIPs: peerAddresses,
|
||||
Endpoint: peer["endpoint"].(string),
|
||||
LastHandshake: peerHandshake,
|
||||
ReceivedBytes: int(peer["receive_bytes"].(float64)),
|
||||
TransmittedBytes: int(peer["transmit_bytes"].(float64)),
|
||||
}
|
||||
|
||||
if withClientDetails {
|
||||
for _, client := range clients {
|
||||
if client.PublicKey != newClientStatus.PublicKey {
|
||||
continue
|
||||
}
|
||||
|
||||
newClientStatus.Name = client.Name
|
||||
newClientStatus.Email = client.Email
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
clientStatus = append(clientStatus, newClientStatus)
|
||||
}
|
||||
return clientStatus, nil
|
||||
}
|
29
model/status.go
Normal file
29
model/status.go
Normal file
@ -0,0 +1,29 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ClientStatus structure
|
||||
type ClientStatus struct {
|
||||
PublicKey string `json:"publicKey"`
|
||||
HasPresharedKey bool `json:"hasPresharedKey"`
|
||||
ProtocolVersion int `json:"protocolVersion"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Connected bool `json:"connected"`
|
||||
AllowedIPs []string `json:"allowedIPs"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
LastHandshake time.Time `json:"lastHandshake"`
|
||||
ReceivedBytes int `json:"receivedBytes"`
|
||||
TransmittedBytes int `json:"transmittedBytes"`
|
||||
}
|
||||
|
||||
// InterfaceStatus structure
|
||||
type InterfaceStatus struct {
|
||||
Name string `json:"name"`
|
||||
DeviceType string `json:"type"`
|
||||
ListenPort int `json:"listenPort"`
|
||||
NumberOfPeers int `json:"numPeers"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
}
|
@ -15,6 +15,10 @@
|
||||
Server
|
||||
<v-icon right dark>mdi-vpn</v-icon>
|
||||
</v-btn>
|
||||
<v-btn to="/status">
|
||||
Status
|
||||
<v-icon right dark>mdi-chart-bar</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
|
||||
<v-menu
|
||||
|
175
ui/src/components/Status.vue
Normal file
175
ui/src/components/Status.vue
Normal file
@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row v-if="dataLoaded">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
WireGuard Interface Status: {{ interface.name }}
|
||||
</v-card-title>
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>Public Key: {{ interface.publicKey }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>Listening Port: {{ interface.listenPort }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>Device Type: {{ interface.type }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>Number of Peers: {{ interface.numPeers }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="dataLoaded">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
WireGuard Client Status
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
append-icon="mdi-magnify"
|
||||
label="Search"
|
||||
single-line
|
||||
hide-details
|
||||
></v-text-field>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="success"
|
||||
@click="reload"
|
||||
>
|
||||
Reload
|
||||
<v-icon right dark>mdi-reload</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="clients"
|
||||
:search="search"
|
||||
>
|
||||
<template v-slot:item.address="{ item }">
|
||||
<v-chip
|
||||
v-for="(ip, i) in item.address"
|
||||
:key="i"
|
||||
color="indigo"
|
||||
text-color="white"
|
||||
>
|
||||
<v-icon left>mdi-ip-network</v-icon>
|
||||
{{ ip }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.tags="{ item }">
|
||||
<v-chip
|
||||
v-for="(tag, i) in item.tags"
|
||||
:key="i"
|
||||
color="blue-grey"
|
||||
text-color="white"
|
||||
>
|
||||
<v-icon left>mdi-tag</v-icon>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.created="{ item }">
|
||||
<v-row>
|
||||
<p>At {{ item.created | formatDate }} by {{ item.createdBy }}</p>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-slot:item.updated="{ item }">
|
||||
<v-row>
|
||||
<p>At {{ item.updated | formatDate }} by {{ item.updatedBy }}</p>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-slot:item.action="{ item }">
|
||||
<v-row>
|
||||
<v-icon
|
||||
class="pr-1 pl-1"
|
||||
@click.stop="startUpdate(item)"
|
||||
>
|
||||
mdi-square-edit-outline
|
||||
</v-icon>
|
||||
<v-icon
|
||||
class="pr-1 pl-1"
|
||||
@click.stop="forceFileDownload(item)"
|
||||
>
|
||||
mdi-cloud-download-outline
|
||||
</v-icon>
|
||||
<v-icon
|
||||
class="pr-1 pl-1"
|
||||
@click.stop="email(item)"
|
||||
>
|
||||
mdi-email-send-outline
|
||||
</v-icon>
|
||||
<v-icon
|
||||
class="pr-1 pl-1"
|
||||
@click="remove(item)"
|
||||
>
|
||||
mdi-trash-can-outline
|
||||
</v-icon>
|
||||
<v-switch
|
||||
dark
|
||||
class="pr-1 pl-1"
|
||||
color="success"
|
||||
v-model="item.enable"
|
||||
v-on:change="update(item)"
|
||||
/>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
No stats available...
|
||||
</v-card-title>
|
||||
<v-card-text>{{ error }}</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Status',
|
||||
|
||||
data: () => ({
|
||||
search: '',
|
||||
headers: [
|
||||
{ text: 'Connected', value: 'connected', },
|
||||
{ text: 'Name', value: 'name', },
|
||||
{ text: 'Endpoint', value: 'endpoint', },
|
||||
{ text: 'IP addresses', value: 'allowedIPs', sortable: false, },
|
||||
{ text: 'Received Bytes', value: 'receivedBytes', },
|
||||
{ text: 'Transmitted Bytes', value: 'transmittedBytes', },
|
||||
{ text: 'Last Handshake', value: 'lastHandshake',} ,
|
||||
],
|
||||
}),
|
||||
|
||||
computed:{
|
||||
...mapGetters({
|
||||
interface: 'status/interfaceStatus',
|
||||
clients: 'status/clientStatus',
|
||||
error: 'status/error',
|
||||
}),
|
||||
dataLoaded: function () {
|
||||
return this.interface != null && this.interface.name !== "";
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.readStatus()
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('status', {
|
||||
readStatus: 'read',
|
||||
}),
|
||||
|
||||
reload() {
|
||||
this.readStatus()
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
@ -24,7 +24,17 @@ const routes = [
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/status',
|
||||
name: 'status',
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "Status" */ '../views/Status.vue')
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
|
@ -3,6 +3,7 @@ import Vuex from 'vuex'
|
||||
import auth from "./modules/auth";
|
||||
import client from "./modules/client";
|
||||
import server from "./modules/server";
|
||||
import status from "./modules/status";
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
@ -14,6 +15,7 @@ export default new Vuex.Store({
|
||||
modules: {
|
||||
auth,
|
||||
client,
|
||||
server
|
||||
server,
|
||||
status,
|
||||
}
|
||||
})
|
||||
|
77
ui/src/store/modules/status.js
Normal file
77
ui/src/store/modules/status.js
Normal file
@ -0,0 +1,77 @@
|
||||
import ApiService from "../../services/api.service";
|
||||
|
||||
const state = {
|
||||
error: null,
|
||||
interfaceStatus: null,
|
||||
clientStatus: [],
|
||||
version: '_ci_build_not_run_properly_',
|
||||
}
|
||||
|
||||
const getters = {
|
||||
error(state) {
|
||||
return state.error;
|
||||
},
|
||||
|
||||
interfaceStatus(state) {
|
||||
return state.interfaceStatus;
|
||||
},
|
||||
|
||||
clientStatus(state) {
|
||||
return state.clientStatus;
|
||||
},
|
||||
|
||||
version(state) {
|
||||
return state.version;
|
||||
},
|
||||
}
|
||||
|
||||
const actions = {
|
||||
error({ commit }, error){
|
||||
commit('error', error)
|
||||
},
|
||||
|
||||
read({ commit }){
|
||||
ApiService.get("/status/interface")
|
||||
.then(resp => {
|
||||
commit('interfaceStatus', resp)
|
||||
})
|
||||
.catch(err => {
|
||||
commit('interfaceStatus', null);
|
||||
commit('error', err)
|
||||
});
|
||||
ApiService.get("/status/clients")
|
||||
.then(resp => {
|
||||
commit('clientStatus', resp)
|
||||
})
|
||||
.catch(err => {
|
||||
commit('clientStatus', []);
|
||||
commit('error', err)
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
error(state, error) {
|
||||
state.error = error;
|
||||
},
|
||||
|
||||
interfaceStatus(state, interfaceStatus){
|
||||
state.interfaceStatus = interfaceStatus
|
||||
},
|
||||
|
||||
clientStatus(state, clientStatus){
|
||||
state.clientStatus = clientStatus
|
||||
},
|
||||
|
||||
version(state, version){
|
||||
state.version = version
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
16
ui/src/views/Status.vue
Normal file
16
ui/src/views/Status.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<v-content>
|
||||
<Status/>
|
||||
</v-content>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Status from '../components/Status'
|
||||
|
||||
export default {
|
||||
name: 'status',
|
||||
components: {
|
||||
Status
|
||||
}
|
||||
}
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user